diff --git a/CHANGES.md b/CHANGES.md
index 459417162e..86e915656e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -17,6 +17,7 @@
- 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)
- 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
diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs
index abd34f5c13..6df0ee299a 100644
--- a/frontend/src/app/main/ui/ds.cljs
+++ b/frontend/src/app/main/ui/ds.cljs
@@ -18,7 +18,8 @@
[app.main.ui.ds.foundations.typography :refer [typography-list]]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[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.notifications.actionable :refer [actionable*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
diff --git a/frontend/src/app/main/ui/ds/controls/input.cljs b/frontend/src/app/main/ui/ds/controls/input.cljs
index 93674b4da5..4e31d87dc5 100644
--- a/frontend/src/app/main/ui/ds/controls/input.cljs
+++ b/frontend/src/app/main/ui/ds/controls/input.cljs
@@ -5,50 +5,62 @@
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.ds.controls.input
- (:require-macros
- [app.common.data.macros :as dm]
- [app.main.style :as stl])
+ (:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
+ [app.common.data.macros :as dm]
[app.main.constants :refer [max-input-length]]
- [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
- [app.util.dom :as dom]
+ [app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
+ [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]))
(def ^:private schema:input
[:map
+ [:id {:optional true} :string]
+ [:label {:optional true} :string]
[:class {:optional true} :string]
+ [:is-optional {:optional true} :boolean]
+ [:placeholder {:optional true} :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
[:type {:optional true} :string]
[: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/props :obj
::mf/forward-ref true
::mf/schema schema:input}
- [{:keys [icon class type max-length variant] :rest props} ref]
- (let [ref (or ref (mf/use-ref))
- type (d/nilv type "text")
- props (mf/spread-props props
- {:class (stl/css-case
- :input true
- :input-with-icon (some? icon))
- :ref ref
- :maxlength (d/nilv max-length (str max-input-length))
- :type type})
+ [{:keys [id class label is-optional type max-length variant hint-message hint-type children] :rest props} ref]
+ (let [id (or id (mf/use-id))
+ variant (d/nilv variant "dense")
+ is-optional (d/nilv is-optional false)
+ type (d/nilv type "text")
+ max-length (d/nilv max-length max-input-length)
+ has-hint (and (some? hint-message) (not (str/blank? hint-message)))
+ has-label (not (str/blank? label))
+ ref (or ref (mf/use-ref))
+ 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]]))
diff --git a/frontend/src/app/main/ui/ds/controls/input.mdx b/frontend/src/app/main/ui/ds/controls/input.mdx
index 1ecb0e937e..47e6b303c2 100644
--- a/frontend/src/app/main/ui/ds/controls/input.mdx
+++ b/frontend/src/app/main/ui/ds/controls/input.mdx
@@ -5,13 +5,10 @@ import * as InputStories from "./input.stories";
# Input
-The `input*` component is a wrapper to the HTML `` element with custom styling
-and additional elements that adds context and, in some cases, adds extra
-functionality.
+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.
-
## Technical notes
### Icons
@@ -29,8 +26,6 @@ These are available in the `app.main.ds.foundations.assets.icon` namespace.
[:> input* {:icon i/effects}]
```
-
-
## Usage guidelines (design)
### Where to use
diff --git a/frontend/src/app/main/ui/ds/controls/input.scss b/frontend/src/app/main/ui/ds/controls/input.scss
index 0eeb49359b..a78df29a1b 100644
--- a/frontend/src/app/main/ui/ds/controls/input.scss
+++ b/frontend/src/app/main/ui/ds/controls/input.scss
@@ -4,98 +4,11 @@
//
// Copyright (c) KALEIDOS INC
-@use "../_borders.scss" as *;
-@use "../_sizes.scss" as *;
-@use "../typography.scss" as *;
+@use "../spacing.scss" as *;
-.container {
- --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;
-
- display: inline-flex;
- column-gap: var(--sp-xs);
- align-items: center;
- position: relative;
+.input-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-xs);
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);
}
diff --git a/frontend/src/app/main/ui/ds/controls/input.stories.jsx b/frontend/src/app/main/ui/ds/controls/input.stories.jsx
index 0e23bffe73..f80a8a97bc 100644
--- a/frontend/src/app/main/ui/ds/controls/input.stories.jsx
+++ b/frontend/src/app/main/ui/ds/controls/input.stories.jsx
@@ -14,34 +14,74 @@ export default {
title: "Controls/Input",
component: Components.Input,
argTypes: {
+ defaultValue: {
+ control: { type: "text" },
+ },
+ label: {
+ control: { type: "text" },
+ },
+ placeholder: {
+ control: { type: "text" },
+ },
+ isOptional: { control: { type: "boolean" } },
icon: {
options: icons,
control: { type: "select" },
},
- value: {
+ type: {
+ options: ["text", "email", "password"],
+ control: { type: "select" },
+ },
+ hintMessage: {
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: {
+ 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,
- value: "Lorem ipsum",
+ },
+ parameters: {
+ controls: { exclude: ["id"] },
},
render: ({ ...args }) => ,
};
export const Default = {};
-export const WithIcon = {
+export const Dense = {
args: {
- icon: "effects",
+ variant: "dense",
},
};
-export const WithPlaceholder = {
+export const Comfortable = {
args: {
- icon: "effects",
- value: undefined,
- placeholder: "Mixed",
+ variant: "comfortable",
+ },
+};
+
+export const Seamless = {
+ args: {
+ variant: "seamless",
},
};
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/hint_message.cljs b/frontend/src/app/main/ui/ds/controls/utilities/hint_message.cljs
new file mode 100644
index 0000000000..cff5571ebf
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/utilities/hint_message.cljs
@@ -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])]))
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss b/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss
new file mode 100644
index 0000000000..c80cf0e1a5
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/utilities/hint_message.scss
@@ -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);
+}
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs
new file mode 100644
index 0000000000..3266f15cbd
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs
@@ -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])]))
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss
new file mode 100644
index 0000000000..4094006125
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.scss
@@ -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);
+}
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/label.cljs b/frontend/src/app/main/ui/ds/controls/utilities/label.cljs
new file mode 100644
index 0000000000..1b5e06cedb
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/utilities/label.cljs
@@ -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)"])]]))
diff --git a/frontend/src/app/main/ui/ds/controls/utilities/label.scss b/frontend/src/app/main/ui/ds/controls/utilities/label.scss
new file mode 100644
index 0000000000..572a1e3bb2
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/controls/utilities/label.scss
@@ -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);
+}
diff --git a/frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens.cljs b/frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens_value.cljs
similarity index 83%
rename from frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens.cljs
rename to frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens_value.cljs
index 3746e32175..44e9379589 100644
--- a/frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens_value.cljs
@@ -4,7 +4,7 @@
;;
;; 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
[app.common.data :as d]
@@ -13,9 +13,8 @@
[app.main.ui.ds.controls.input :refer [input*]]
[rumext.v2 :as mf]))
-(def ^:private schema::input-tokens
+(def ^:private schema::input-tokens-value
[:map
- [:id :string]
[:label :string]
[:placeholder {:optional true} :string]
[:default-value {:optional true} [:maybe :string]]
@@ -24,19 +23,20 @@
[:error {:optional true} :boolean]
[:value {:optional true} :string]])
-(mf/defc input-tokens*
+(mf/defc input-tokens-value*
{::mf/props :obj
::mf/forward-ref true
- ::mf/schema schema::input-tokens}
- [{:keys [class label id max-length error value children] :rest props} ref]
- (let [ref (or ref (mf/use-ref))
+ ::mf/schema schema::input-tokens-value}
+ [{:keys [class label max-length error value children] :rest props} ref]
+ (let [id (mf/use-id)
+ input-ref (mf/use-ref)
props (mf/spread-props props {:id id
:type "text"
:class (stl/css :input)
:aria-invalid error
:max-length (d/nilv max-length max-input-length)
:value value
- :ref ref})]
+ :ref (or ref input-ref)})]
[:div {:class (dm/str class " " (stl/css-case :wrapper true
:input-error error))}
[:label {:for id :class (stl/css :label)} label]
diff --git a/frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens.scss b/frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens_value.scss
similarity index 97%
rename from frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens.scss
rename to frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens_value.scss
index 71d0c00252..ea4ad5106f 100644
--- a/frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/components/controls/input_tokens_value.scss
@@ -34,6 +34,7 @@
.input-wrapper {
display: flex;
+ flex: 1;
align-items: center;
&:has(.input-swatch) {
@@ -51,3 +52,7 @@
inset-inline-start: var(--sp-s);
z-index: 2;
}
+
+.input {
+ flex: 1;
+}
diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs
index 606b070936..9339563e11 100644
--- a/frontend/src/app/main/ui/workspace/tokens/form.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs
@@ -12,6 +12,7 @@
[app.common.data.macros :as dm]
[app.common.files.tokens :as cft]
[app.common.types.tokens-lib :as ctob]
+ [app.main.constants :refer [max-input-length]]
[app.main.data.modal :as modal]
[app.main.data.style-dictionary :as sd]
[app.main.data.tinycolor :as tinycolor]
@@ -23,6 +24,8 @@
[app.main.refs :as refs]
[app.main.store :as st]
[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.typography.heading :refer [heading*]]
[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.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-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.functions :as uf]
[app.util.i18n :refer [tr]]
@@ -277,6 +280,7 @@
token-name-ref (mf/use-var (:name token))
name-ref (mf/use-ref nil)
name-errors (mf/use-state nil)
+
validate-name
(mf/use-fn
(mf/deps selected-set-tokens-tree)
@@ -405,20 +409,22 @@
;; Description
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 %))
on-update-description-debounced (mf/use-fn
(uf/debounce (fn [e]
(let [value (dom/get-target-val e)
errors (validate-descripion value)]
- (reset! description-errors errors)))))
+ (reset! description-errors* errors)))))
on-update-description
(mf/use-fn
(mf/deps on-update-description-debounced)
(fn [e]
(reset! description-ref (dom/get-target-val e))
(on-update-description-debounced e)))
- valid-description-field? (not @description-errors)
+ valid-description-field? (not description-errors)
;; Form
disabled? (or (not valid-name-field?)
@@ -523,26 +529,26 @@
[:div {:class (stl/css :input-row)}
(let [token-title (str/lower (:title token-properties))]
- [:> input-tokens*
- {:id "token-name"
- :placeholder (tr "workspace.token.enter-token-name", token-title)
- :error (boolean @name-errors)
- :auto-focus true
- :label (tr "workspace.token.token-name")
- :default-value @token-name-ref
- :ref name-ref
- :max-length 256
- :on-blur on-blur-name
- :on-change on-update-name}])
+ [:> input* {:id "token-name"
+ :label (tr "workspace.token.token-name")
+ :placeholder (tr "workspace.token.enter-token-name", token-title)
+ :max-length max-input-length
+ :variant "comfortable"
+ :auto-focus true
+ :default-value @token-name-ref
+ :ref name-ref
+ :on-blur on-blur-name
+ :on-change on-update-name}])
(for [error (->> (:errors @name-errors)
(map #(-> (assoc @name-errors :errors [%])
- (me/humanize))))]
- [:> text* {:as "p"
- :key error
- :typography "body-small"
- :class (stl/css :error)}
- error])
+ (me/humanize)))
+ (map first))]
+
+ [:> hint-message* {:key error
+ :message error
+ :type "error"
+ :id "token-name-hint"}])
(when (and warning-name-change? (= action "edit"))
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
@@ -550,7 +556,7 @@
{:level :warning :appearance :ghost} (tr "workspace.token.warning-name-change")]])]
[:div {:class (stl/css :input-row)}
- [:> input-tokens*
+ [:> input-tokens-value*
{:id "token-value"
:placeholder (tr "workspace.token.token-value-enter")
:label (tr "workspace.token.token-value")
@@ -567,21 +573,14 @@
[:> ramp* {:color (some-> color (tinycolor/valid-color))
:on-change on-update-color}])
[:& token-value-or-errors {:result-or-errors token-resolve-result}]]
-
[:div {:class (stl/css :input-row)}
- [:> input-tokens*
- {:id "token-description"
- :placeholder (tr "workspace.token.enter-token-description")
- :label (tr "workspace.token.token-description")
- :max-length 256
- :default-value @description-ref
- :on-blur 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)])]
+ [:> input* {:label (tr "workspace.token.token-description")
+ :placeholder (tr "workspace.token.enter-token-description")
+ :max-length max-input-length
+ :variant "comfortable"
+ :default-value @description-ref
+ :on-blur on-update-description
+ :on-change on-update-description}]]
[:div {:class (stl/css-case :button-row true
:with-delete (= action "edit"))}
diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs
index 0b8c751e70..f6abbdbc21 100644
--- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs
@@ -10,6 +10,7 @@
[app.common.data.macros :as dm]
[app.common.logic.tokens :as clt]
[app.common.types.tokens-lib :as ctob]
+ [app.main.constants :refer [max-input-length]]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[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.icon-button :refer [icon-button*]]
[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.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[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.util.dom :as dom]
[app.util.i18n :refer [tr]]
@@ -202,14 +203,13 @@
:on-change on-update-group}]]
[:div {:class (stl/css :group-input-wrapper)}
- [:> input-tokens* {:id "theme-input"
- :label (tr "workspace.token.label.theme")
- :type "text"
- :max-length 256
- :placeholder (tr "workspace.token.label.theme-placeholder")
- :on-change on-update-name
- :value (mf/ref-val theme-name-ref)
- :auto-focus true}]]]))
+ [:> input* {:label (tr "workspace.token.label.theme")
+ :placeholder (tr "workspace.token.label.theme-placeholder")
+ :max-length max-input-length
+ :variant "comfortable"
+ :default-value (mf/ref-val theme-name-ref)
+ :auto-focus true
+ :on-change on-update-name}]]]))
(mf/defc theme-modal-buttons*
[{:keys [close-modal on-save-form disabled?] :as props}]