mirror of
https://github.com/penpot/penpot.git
synced 2025-06-04 13:31:39 +02:00
♻️ 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:
parent
f8602810eb
commit
486f036a11
16 changed files with 470 additions and 190 deletions
|
@ -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*]]
|
||||
|
|
|
@ -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]]))
|
||||
|
|
|
@ -5,13 +5,10 @@ import * as InputStories from "./input.stories";
|
|||
|
||||
# Input
|
||||
|
||||
The `input*` component is a wrapper to the HTML `<input>` 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.
|
||||
|
||||
<Canvas of={InputStories.Default} />
|
||||
|
||||
|
||||
## Technical notes
|
||||
|
||||
### Icons
|
||||
|
@ -29,8 +26,6 @@ These are available in the `app.main.ds.foundations.assets.icon` namespace.
|
|||
[:> input* {:icon i/effects}]
|
||||
```
|
||||
|
||||
<Canvas of={InputStories.WithIcon} />
|
||||
|
||||
## Usage guidelines (design)
|
||||
|
||||
### Where to use
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 }) => <Input {...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",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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])]))
|
|
@ -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);
|
||||
}
|
|
@ -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])]))
|
121
frontend/src/app/main/ui/ds/controls/utilities/input_field.scss
Normal file
121
frontend/src/app/main/ui/ds/controls/utilities/input_field.scss
Normal 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);
|
||||
}
|
31
frontend/src/app/main/ui/ds/controls/utilities/label.cljs
Normal file
31
frontend/src/app/main/ui/ds/controls/utilities/label.cljs
Normal 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)"])]]))
|
28
frontend/src/app/main/ui/ds/controls/utilities/label.scss
Normal file
28
frontend/src/app/main/ui/ds/controls/utilities/label.scss
Normal 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);
|
||||
}
|
|
@ -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]
|
|
@ -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;
|
||||
}
|
|
@ -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"))}
|
||||
|
|
|
@ -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}]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue