♻️ Redesign form input tokens (#6294)

* ♻️ Redesign form input tokens

* ♻️ Redesign form input tokens

---------

Co-authored-by: Xavier Julian <xaviju@proton.me>
This commit is contained in:
Xaviju 2025-05-05 09:05:14 +02:00 committed by GitHub
parent f8602810eb
commit 486f036a11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 470 additions and 190 deletions

View file

@ -17,6 +17,7 @@
- Duplicate token sets [Taiga #10694](https://tree.taiga.io/project/penpot/issue/10694) - Duplicate token sets [Taiga #10694](https://tree.taiga.io/project/penpot/issue/10694)
- Add set selection in create Token themes flow [Taiga #10746](https://tree.taiga.io/project/penpot/issue/10746) - Add set selection in create Token themes flow [Taiga #10746](https://tree.taiga.io/project/penpot/issue/10746)
- Display indicator on not active sets [Taiga #10668](https://tree.taiga.io/project/penpot/issue/10668) - Display indicator on not active sets [Taiga #10668](https://tree.taiga.io/project/penpot/issue/10668)
- Create `input*` wrapper component, and `label*`, `input-field*` and `hint-message*` components [Taiga #10713](https://tree.taiga.io/project/penpot/us/10713)
### :bug: Bugs fixed ### :bug: Bugs fixed

View file

@ -18,7 +18,8 @@
[app.main.ui.ds.foundations.typography :refer [typography-list]] [app.main.ui.ds.foundations.typography :refer [typography-list]]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon* token-status-list]] [app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*
token-status-list]]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]] [app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.ds.notifications.actionable :refer [actionable*]] [app.main.ui.ds.notifications.actionable :refer [actionable*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]] [app.main.ui.ds.notifications.context-notification :refer [context-notification*]]

View file

@ -5,50 +5,62 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.main.ui.ds.controls.input (ns app.main.ui.ds.controls.input
(:require-macros (:require-macros [app.main.style :as stl])
[app.common.data.macros :as dm]
[app.main.style :as stl])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.constants :refer [max-input-length]] [app.main.constants :refer [max-input-length]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]] [app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
[app.util.dom :as dom] [app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
[app.main.ui.ds.controls.utilities.label :refer [label*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]]
[cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private schema:input (def ^:private schema:input
[:map [:map
[:id {:optional true} :string]
[:label {:optional true} :string]
[:class {:optional true} :string] [:class {:optional true} :string]
[:is-optional {:optional true} :boolean]
[:placeholder {:optional true} :string]
[:icon {:optional true} [:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]] [:and :string [:fn #(contains? icon-list %)]]]
[:type {:optional true} :string] [:type {:optional true} :string]
[:max-length {:optional true} :int] [:max-length {:optional true} :int]
[:variant {:optional true} :string]]) [:variant {:optional true} [:maybe [:enum "seamless" "dense" "comfortable"]]]
[:hint-message {:optional true} [:maybe :string]]
[:hint-type {:optional true} [:maybe [:enum "hint" "error" "warning"]]]])
(mf/defc input* (mf/defc input*
{::mf/props :obj {::mf/props :obj
::mf/forward-ref true ::mf/forward-ref true
::mf/schema schema:input} ::mf/schema schema:input}
[{:keys [icon class type max-length variant] :rest props} ref] [{:keys [id class label is-optional type max-length variant hint-message hint-type children] :rest props} ref]
(let [ref (or ref (mf/use-ref)) (let [id (or id (mf/use-id))
type (d/nilv type "text") variant (d/nilv variant "dense")
props (mf/spread-props props is-optional (d/nilv is-optional false)
{:class (stl/css-case type (d/nilv type "text")
:input true max-length (d/nilv max-length max-input-length)
:input-with-icon (some? icon)) has-hint (and (some? hint-message) (not (str/blank? hint-message)))
:ref ref has-label (not (str/blank? label))
:maxlength (d/nilv max-length (str max-input-length)) ref (or ref (mf/use-ref))
:type type}) props (mf/spread-props props {:ref ref
:type type
:id id
:hint-type hint-type
:max-length max-length
:has-hint has-hint
:variant variant})]
[:div {:class (dm/str class " " (stl/css-case :input-wrapper true
:variant-dense (= variant "dense")
:variant-comfortable (= variant "comfortable")
:has-hint has-hint))}
(when has-label
[:> label* {:for id :is-optional is-optional} label])
[:> input-field* props children]
(when has-hint
[:> hint-message* {:id id
:message hint-message
:type hint-type}])]))
on-icon-click
(mf/use-fn
(mf/deps ref)
(fn [_event]
(let [input-node (mf/ref-val ref)]
(dom/select-node input-node)
(dom/focus! input-node))))]
[:> :span {:class (dm/str class " " (stl/css-case :container true
:input-seamless (= variant "seamless")))}
(when (some? icon)
[:> icon* {:icon-id icon :class (stl/css :icon) :on-click on-icon-click}])
[:> :input props]]))

View file

@ -5,13 +5,10 @@ import * as InputStories from "./input.stories";
# Input # Input
The `input*` component is a wrapper to the HTML `<input>` element with custom styling The `input*` component is a wrapper composed of `label*`, `input-field*`, and `hint-message*` components, functioning as a form element that adapts its UI based on configuration, making it suitable for different areas of the interface.
and additional elements that adds context and, in some cases, adds extra
functionality.
<Canvas of={InputStories.Default} /> <Canvas of={InputStories.Default} />
## Technical notes ## Technical notes
### Icons ### Icons
@ -29,8 +26,6 @@ These are available in the `app.main.ds.foundations.assets.icon` namespace.
[:> input* {:icon i/effects}] [:> input* {:icon i/effects}]
``` ```
<Canvas of={InputStories.WithIcon} />
## Usage guidelines (design) ## Usage guidelines (design)
### Where to use ### Where to use

View file

@ -4,98 +4,11 @@
// //
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "../_borders.scss" as *; @use "../spacing.scss" as *;
@use "../_sizes.scss" as *;
@use "../typography.scss" as *;
.container { .input-wrapper {
--input-bg-color: var(--color-background-tertiary); display: flex;
--input-fg-color: var(--color-foreground-primary); flex-direction: column;
--input-icon-color: var(--color-foreground-secondary); gap: var(--sp-xs);
--input-outline-color: none;
display: inline-flex;
column-gap: var(--sp-xs);
align-items: center;
position: relative;
inline-size: 100%; inline-size: 100%;
background: var(--input-bg-color);
border-radius: $br-8;
padding: 0 var(--sp-s);
outline-offset: #{$b-1};
outline: $b-1 solid var(--input-outline-color);
&:hover {
--input-bg-color: var(--color-background-quaternary);
}
&:has(*:focus-visible) {
--input-bg-color: var(--color-background-primary);
--input-outline-color: var(--color-accent-primary);
}
&:has(*:disabled) {
--input-bg-color: var(--color-background-primary);
--input-outline-color: var(--color-background-quaternary);
}
}
.input-seamless {
--input-bg-color: none;
--input-outline-color: none;
--input-height: auto;
--input-margin: 0;
padding: 0;
border: none;
&:hover {
--input-bg-color: none;
--input-outline-color: none;
}
&:has(*:focus-visible) {
--input-bg-color: none;
--input-outline-color: none;
}
}
.input {
margin: var(--input-margin, unset); // remove settings from global css
padding: 0;
appearance: none;
height: var(--input-height, #{$sz-32});
border: none;
background: none;
inline-size: 100%;
@include use-typography("body-small");
color: var(--input-fg-color);
&:focus-visible {
outline: none;
}
&::selection {
background: var(--color-accent-select);
}
&::placeholder {
--input-fg-color: var(--color-foreground-secondary);
}
&:is(:autofill, :autofill:hover, :autofill:focus, :autofill:active) {
-webkit-text-fill-color: var(--input-fg-color);
-webkit-background-clip: text;
caret-color: var(--input-bg-color);
}
}
.input-with-icon {
margin-inline-start: var(--sp-xxs);
}
.icon {
color: var(--color-foreground-secondary);
} }

View file

@ -14,34 +14,74 @@ export default {
title: "Controls/Input", title: "Controls/Input",
component: Components.Input, component: Components.Input,
argTypes: { argTypes: {
defaultValue: {
control: { type: "text" },
},
label: {
control: { type: "text" },
},
placeholder: {
control: { type: "text" },
},
isOptional: { control: { type: "boolean" } },
icon: { icon: {
options: icons, options: icons,
control: { type: "select" }, control: { type: "select" },
}, },
value: { type: {
options: ["text", "email", "password"],
control: { type: "select" },
},
hintMessage: {
control: { type: "text" }, control: { type: "text" },
}, },
disabled: { control: "boolean" }, hintType: {
options: ["hint", "error", "warning"],
control: { type: "select" },
},
variant: {
options: ["dense", "comfortable"],
control: { type: "select" },
},
disabled: {
control: { type: "boolean" },
},
}, },
args: { args: {
id: "input",
label: "Label",
isOptional: false,
defaultValue: "Value",
placeholder: "Placeholder",
type: "text",
icon: "search",
hintMessage: "This is a hint text to help user.",
hintType: "hint",
variant: "dense",
disabled: false, disabled: false,
value: "Lorem ipsum", },
parameters: {
controls: { exclude: ["id"] },
}, },
render: ({ ...args }) => <Input {...args} />, render: ({ ...args }) => <Input {...args} />,
}; };
export const Default = {}; export const Default = {};
export const WithIcon = { export const Dense = {
args: { args: {
icon: "effects", variant: "dense",
}, },
}; };
export const WithPlaceholder = { export const Comfortable = {
args: { args: {
icon: "effects", variant: "comfortable",
value: undefined, },
placeholder: "Mixed", };
export const Seamless = {
args: {
variant: "seamless",
}, },
}; };

View file

@ -0,0 +1,35 @@
;; 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.ds.controls.utilities.hint-message
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[rumext.v2 :as mf]))
(def ^:private schema::hint-message
[:map
[:message :string]
[:id :string]
[:type {:optional true} [:enum "hint" "error" "warning"]]
[:class {:optional true} :string]])
(mf/defc hint-message*
{::mf/props :obj
::mf/schema schema::hint-message}
[{:keys [id class message type] :rest props}]
(let [type (d/nilv type :hint)]
[:> "div" {:class (dm/str class " " (stl/css-case
:hint-message true
:type-hint (= type "hint")
:type-warning (= type "warning")
:type-error (= type "error")))
:aria-live (when (or (= type "warning") (= type "error")) "polite")}
(when (some? message)
[:span {:class (stl/css :hint-message-text)
:id (str id "-hint")}
message])]))

View file

@ -0,0 +1,27 @@
// 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 "../../typography.scss" as *;
@use "../../colors.scss" as *;
.hint-message {
--hint-color: var(--color-foreground-secondary);
@include use-typography("body-small");
color: var(--hint-color);
}
.type-hint {
--hint-color: var(--color-foreground-secondary);
}
.type-warning {
--hint-color: var(--color-accent-warning);
}
.type-error {
--hint-color: var(--color-foreground-error);
}

View file

@ -0,0 +1,72 @@
;; 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.ds.controls.utilities.input-field
(:require-macros
[app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.constants :refer [max-input-length]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
(def ^:private schema:input-field
[:map
[:class {:optional true} :string]
[:id :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
[:has-hint {:optional true} :boolean]
[:hint-type {:optional true} [:maybe [:enum "hint" "error" "warning"]]]
[:type {:optional true} :string]
[:max-length {:optional true} :int]
[:variant {:optional true} [:enum "seamless" "dense" "comfortable"]]])
(mf/defc input-field*
{::mf/props :obj
::mf/forward-ref true
::mf/schema schema:input-field}
[{:keys [id icon has-hint hint-type class type max-length variant children] :rest props} ref]
(let [input-ref (mf/use-ref)
type (d/nilv type "text")
variant (d/nilv variant "dense")
props (mf/spread-props props
{:class (stl/css-case
:input true
:input-with-icon (some? icon))
:ref (or ref input-ref)
:aria-invalid (when (and has-hint
(= hint-type "error"))
"true")
:aria-describedby (when has-hint
(str id "-hint"))
:type (d/nilv type "text")
:id id
:max-length (d/nilv max-length max-input-length)})
on-icon-click
(mf/use-fn
(mf/deps ref)
(fn [_event]
(let [input-node (mf/ref-val ref)]
(dom/select-node input-node)
(dom/focus! input-node))))]
[:div {:class (dm/str class " " (stl/css-case :input-wrapper true
:has-hint has-hint
:hint-type-hint (= hint-type "hint")
:hint-type-warning (= hint-type "warning")
:hint-type-error (= hint-type "error")
:variant-seamless (= variant "seamless")
:variant-dense (= variant "dense")
:variant-comfortable (= variant "comfortable")))}
(when (some? icon)
[:> icon* {:icon-id icon :class (stl/css :icon) :on-click on-icon-click}])
[:> "input" props]
(when (some? children)
[:div {:class (stl/css :input-actions)} children])]))

View file

@ -0,0 +1,121 @@
// 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 "../../_borders.scss" as *;
@use "../../_sizes.scss" as *;
@use "../../typography.scss" as *;
@use "../../colors.scss" as *;
.input-wrapper {
--input-bg-color: var(--color-background-tertiary);
--input-fg-color: var(--color-foreground-primary);
--input-icon-color: var(--color-foreground-secondary);
--input-outline-color: none;
--input-height: #{$sz-32};
--input-margin: unset;
display: inline-flex;
column-gap: var(--sp-xs);
align-items: center;
position: relative;
inline-size: 100%;
background: var(--input-bg-color);
border-radius: $br-8;
padding: 0 var(--sp-s);
outline-offset: #{$b-1};
outline: $b-1 solid var(--input-outline-color);
&:hover {
--input-bg-color: var(--color-background-quaternary);
}
&:has(*:focus-visible) {
--input-bg-color: var(--color-background-primary);
--input-outline-color: var(--color-accent-primary);
}
&:has(*:disabled) {
--input-bg-color: var(--color-background-primary);
--input-outline-color: var(--color-background-quaternary);
}
}
.variant-dense {
@include use-typography("body-small");
}
.variant-comfortable {
@include use-typography("body-medium");
}
.variant-seamless {
--input-bg-color: none;
--input-outline-color: none;
--input-height: auto;
--input-margin: 0;
padding: 0;
border: none;
&:hover {
--input-bg-color: none;
--input-outline-color: none;
}
&:has(*:focus-visible) {
--input-bg-color: none;
--input-outline-color: none;
}
}
.input {
margin: var(--input-margin); // remove settings from global css
padding: 0;
appearance: none;
height: var(--input-height);
border: none;
background: none;
inline-size: 100%;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
line-height: inherit;
color: var(--input-fg-color);
&:focus-visible {
outline: none;
}
&::selection {
background: var(--color-accent-select);
}
&::placeholder {
--input-fg-color: var(--color-foreground-secondary);
}
&:is(:autofill, :autofill:hover, :autofill:focus, :autofill:active) {
-webkit-text-fill-color: var(--input-fg-color);
-webkit-background-clip: text;
background-clip: text;
caret-color: var(--input-bg-color);
}
}
.input-with-icon {
margin-inline-start: var(--sp-xxs);
}
.hint-type-error:has(.has-hint) {
--input-outline-color: var(--color-foreground-error);
}
.icon {
color: var(--color-foreground-secondary);
}

View file

@ -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
(ns app.main.ui.ds.controls.utilities.label
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[rumext.v2 :as mf]))
(def ^:private schema::label
[:map
[:for :string]
[:is-optional {:optional true} :boolean]
[:class {:optional true} :string]])
(mf/defc label*
{::mf/props :obj
::mf/schema schema::label}
[{:keys [class for is-optional children] :rest props}]
(let [is-optional (or is-optional false)
props (mf/spread-props props {:class (dm/str class " " (stl/css :label))
:for for})]
[:> "label" props
[:*
(when (some? children)
[:span {:class (stl/css :label-text)} children])
(when is-optional
[:span {:class (stl/css :label-optional)} "(Optional)"])]]))

View file

@ -0,0 +1,28 @@
// 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 "../../typography.scss" as *;
@use "../../colors.scss" as *;
@use "../../spacing.scss" as *;
.label {
--label-color: var(--color-foreground-primary);
--label-optional-color: var(--color-foreground-secondary);
@include use-typography("body-small");
color: var(--label-color);
display: flex;
gap: var(--sp-xs);
align-items: center;
}
.label-text {
color: var(--label-color);
}
.label-optional {
color: var(--label-optional-color);
}

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.main.ui.workspace.tokens.components.controls.input-tokens (ns app.main.ui.workspace.tokens.components.controls.input-tokens-value
(: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]
@ -13,9 +13,8 @@
[app.main.ui.ds.controls.input :refer [input*]] [app.main.ui.ds.controls.input :refer [input*]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private schema::input-tokens (def ^:private schema::input-tokens-value
[:map [:map
[:id :string]
[:label :string] [:label :string]
[:placeholder {:optional true} :string] [:placeholder {:optional true} :string]
[:default-value {:optional true} [:maybe :string]] [:default-value {:optional true} [:maybe :string]]
@ -24,19 +23,20 @@
[:error {:optional true} :boolean] [:error {:optional true} :boolean]
[:value {:optional true} :string]]) [:value {:optional true} :string]])
(mf/defc input-tokens* (mf/defc input-tokens-value*
{::mf/props :obj {::mf/props :obj
::mf/forward-ref true ::mf/forward-ref true
::mf/schema schema::input-tokens} ::mf/schema schema::input-tokens-value}
[{:keys [class label id max-length error value children] :rest props} ref] [{:keys [class label max-length error value children] :rest props} ref]
(let [ref (or ref (mf/use-ref)) (let [id (mf/use-id)
input-ref (mf/use-ref)
props (mf/spread-props props {:id id props (mf/spread-props props {:id id
:type "text" :type "text"
:class (stl/css :input) :class (stl/css :input)
:aria-invalid error :aria-invalid error
:max-length (d/nilv max-length max-input-length) :max-length (d/nilv max-length max-input-length)
:value value :value value
:ref ref})] :ref (or ref input-ref)})]
[:div {:class (dm/str class " " (stl/css-case :wrapper true [:div {:class (dm/str class " " (stl/css-case :wrapper true
:input-error error))} :input-error error))}
[:label {:for id :class (stl/css :label)} label] [:label {:for id :class (stl/css :label)} label]

View file

@ -34,6 +34,7 @@
.input-wrapper { .input-wrapper {
display: flex; display: flex;
flex: 1;
align-items: center; align-items: center;
&:has(.input-swatch) { &:has(.input-swatch) {
@ -51,3 +52,7 @@
inset-inline-start: var(--sp-s); inset-inline-start: var(--sp-s);
z-index: 2; z-index: 2;
} }
.input {
flex: 1;
}

View file

@ -12,6 +12,7 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.tokens :as cft] [app.common.files.tokens :as cft]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.style-dictionary :as sd] [app.main.data.style-dictionary :as sd]
[app.main.data.tinycolor :as tinycolor] [app.main.data.tinycolor :as tinycolor]
@ -23,6 +24,8 @@
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
[app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.ds.foundations.typography.text :refer [text*]]
@ -30,7 +33,7 @@
[app.main.ui.workspace.colorpicker :as colorpicker] [app.main.ui.workspace.colorpicker :as colorpicker]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]] [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
[app.main.ui.workspace.tokens.components.controls.input-token-color-bullet :refer [input-token-color-bullet*]] [app.main.ui.workspace.tokens.components.controls.input-token-color-bullet :refer [input-token-color-bullet*]]
[app.main.ui.workspace.tokens.components.controls.input-tokens :refer [input-tokens*]] [app.main.ui.workspace.tokens.components.controls.input-tokens-value :refer [input-tokens-value*]]
[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]]
@ -277,6 +280,7 @@
token-name-ref (mf/use-var (:name token)) token-name-ref (mf/use-var (:name token))
name-ref (mf/use-ref nil) name-ref (mf/use-ref nil)
name-errors (mf/use-state nil) name-errors (mf/use-state nil)
validate-name validate-name
(mf/use-fn (mf/use-fn
(mf/deps selected-set-tokens-tree) (mf/deps selected-set-tokens-tree)
@ -405,20 +409,22 @@
;; Description ;; Description
description-ref (mf/use-var (:description token)) description-ref (mf/use-var (:description token))
description-errors (mf/use-state nil) description-errors* (mf/use-state nil)
description-errors (deref description-errors*)
validate-descripion (mf/use-fn #(m/explain token-description-schema %)) validate-descripion (mf/use-fn #(m/explain token-description-schema %))
on-update-description-debounced (mf/use-fn on-update-description-debounced (mf/use-fn
(uf/debounce (fn [e] (uf/debounce (fn [e]
(let [value (dom/get-target-val e) (let [value (dom/get-target-val e)
errors (validate-descripion value)] errors (validate-descripion value)]
(reset! description-errors errors))))) (reset! description-errors* errors)))))
on-update-description on-update-description
(mf/use-fn (mf/use-fn
(mf/deps on-update-description-debounced) (mf/deps on-update-description-debounced)
(fn [e] (fn [e]
(reset! description-ref (dom/get-target-val e)) (reset! description-ref (dom/get-target-val e))
(on-update-description-debounced e))) (on-update-description-debounced e)))
valid-description-field? (not @description-errors) valid-description-field? (not description-errors)
;; Form ;; Form
disabled? (or (not valid-name-field?) disabled? (or (not valid-name-field?)
@ -523,26 +529,26 @@
[:div {:class (stl/css :input-row)} [:div {:class (stl/css :input-row)}
(let [token-title (str/lower (:title token-properties))] (let [token-title (str/lower (:title token-properties))]
[:> input-tokens* [:> input* {:id "token-name"
{:id "token-name" :label (tr "workspace.token.token-name")
:placeholder (tr "workspace.token.enter-token-name", token-title) :placeholder (tr "workspace.token.enter-token-name", token-title)
:error (boolean @name-errors) :max-length max-input-length
:auto-focus true :variant "comfortable"
:label (tr "workspace.token.token-name") :auto-focus true
:default-value @token-name-ref :default-value @token-name-ref
:ref name-ref :ref name-ref
:max-length 256 :on-blur on-blur-name
:on-blur on-blur-name :on-change on-update-name}])
:on-change on-update-name}])
(for [error (->> (:errors @name-errors) (for [error (->> (:errors @name-errors)
(map #(-> (assoc @name-errors :errors [%]) (map #(-> (assoc @name-errors :errors [%])
(me/humanize))))] (me/humanize)))
[:> text* {:as "p" (map first))]
:key error
:typography "body-small" [:> hint-message* {:key error
:class (stl/css :error)} :message error
error]) :type "error"
:id "token-name-hint"}])
(when (and warning-name-change? (= action "edit")) (when (and warning-name-change? (= action "edit"))
[:div {:class (stl/css :warning-name-change-notification-wrapper)} [:div {:class (stl/css :warning-name-change-notification-wrapper)}
@ -550,7 +556,7 @@
{:level :warning :appearance :ghost} (tr "workspace.token.warning-name-change")]])] {:level :warning :appearance :ghost} (tr "workspace.token.warning-name-change")]])]
[:div {:class (stl/css :input-row)} [:div {:class (stl/css :input-row)}
[:> input-tokens* [:> input-tokens-value*
{:id "token-value" {:id "token-value"
:placeholder (tr "workspace.token.token-value-enter") :placeholder (tr "workspace.token.token-value-enter")
:label (tr "workspace.token.token-value") :label (tr "workspace.token.token-value")
@ -567,21 +573,14 @@
[:> ramp* {:color (some-> color (tinycolor/valid-color)) [:> ramp* {:color (some-> color (tinycolor/valid-color))
:on-change on-update-color}]) :on-change on-update-color}])
[:& token-value-or-errors {:result-or-errors token-resolve-result}]] [:& token-value-or-errors {:result-or-errors token-resolve-result}]]
[:div {:class (stl/css :input-row)} [:div {:class (stl/css :input-row)}
[:> input-tokens* [:> input* {:label (tr "workspace.token.token-description")
{:id "token-description" :placeholder (tr "workspace.token.enter-token-description")
:placeholder (tr "workspace.token.enter-token-description") :max-length max-input-length
:label (tr "workspace.token.token-description") :variant "comfortable"
:max-length 256 :default-value @description-ref
:default-value @description-ref :on-blur on-update-description
:on-blur on-update-description :on-change on-update-description}]]
:on-change on-update-description}]
(when @description-errors
[:> text* {:as "p"
:typography "body-small"
:class (stl/css :error)}
(me/humanize @description-errors)])]
[:div {:class (stl/css-case :button-row true [:div {:class (stl/css-case :button-row true
:with-delete (= action "edit"))} :with-delete (= action "edit"))}

View file

@ -10,6 +10,7 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.logic.tokens :as clt] [app.common.logic.tokens :as clt]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.data.workspace.tokens.library-edit :as dwtl]
@ -19,11 +20,11 @@
[app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.combobox :refer [combobox*]] [app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.workspace.tokens.components.controls.input-tokens :refer [input-tokens*]]
[app.main.ui.workspace.tokens.sets :as wts] [app.main.ui.workspace.tokens.sets :as wts]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
@ -202,14 +203,13 @@
:on-change on-update-group}]] :on-change on-update-group}]]
[:div {:class (stl/css :group-input-wrapper)} [:div {:class (stl/css :group-input-wrapper)}
[:> input-tokens* {:id "theme-input" [:> input* {:label (tr "workspace.token.label.theme")
:label (tr "workspace.token.label.theme") :placeholder (tr "workspace.token.label.theme-placeholder")
:type "text" :max-length max-input-length
:max-length 256 :variant "comfortable"
:placeholder (tr "workspace.token.label.theme-placeholder") :default-value (mf/ref-val theme-name-ref)
:on-change on-update-name :auto-focus true
:value (mf/ref-val theme-name-ref) :on-change on-update-name}]]]))
:auto-focus true}]]]))
(mf/defc theme-modal-buttons* (mf/defc theme-modal-buttons*
[{:keys [close-modal on-save-form disabled?] :as props}] [{:keys [close-modal on-save-form disabled?] :as props}]