mirror of
https://github.com/penpot/penpot.git
synced 2025-04-30 13:46:19 +02:00
♻️ refactor swatch component
This commit is contained in:
parent
0c80bf76b8
commit
d980ff05cd
3 changed files with 129 additions and 112 deletions
|
@ -7,70 +7,77 @@
|
||||||
|
|
||||||
(ns app.main.ui.ds.utilities.swatch
|
(ns app.main.ui.ds.utilities.swatch
|
||||||
(:require-macros
|
(:require-macros
|
||||||
|
|
||||||
[app.main.style :as stl])
|
[app.main.style :as stl])
|
||||||
|
|
||||||
(:require
|
(:require
|
||||||
|
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.json :as json]
|
||||||
|
[app.common.schema :as sm]
|
||||||
|
[app.common.types.color :as ct]
|
||||||
|
[app.config :as cfg]
|
||||||
|
[app.util.color :as uc]
|
||||||
|
[app.util.i18n :refer [tr]]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(def ^:private schema:swatch
|
(def ^:private schema:swatch
|
||||||
[:map
|
[:map {:title "SchemaSwatch"}
|
||||||
[:background :string]
|
[:background {:optional true} ct/schema:color]
|
||||||
[:class {:optional true} :string]
|
[:class {:optional true} :string]
|
||||||
[:format {:optional true} [:enum "square" "rounded"]]
|
|
||||||
[:size {:optional true} [:enum "small" "medium"]]
|
[:size {:optional true} [:enum "small" "medium"]]
|
||||||
[:active {:optional true} :boolean]
|
[:active {:optional true} :boolean]
|
||||||
[:on-click {:optional true} fn?]])
|
[:on-click {:optional true} fn?]])
|
||||||
|
|
||||||
(def hex-regex #"^#(?:[0-9a-fA-F]{3}){1,2}$")
|
(defn- color-title
|
||||||
(def rgb-regex #"^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$")
|
[color-item]
|
||||||
(def hsl-regex #"^hsl\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%\)$")
|
(let [name (:name color-item)
|
||||||
(def hsla-regex #"^hsla\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%,\s*(0|1|0?\.\d+)\)$")
|
path (:path color-item)
|
||||||
(def rgba-regex #"^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(0|1|0?\.\d+)\)$")
|
path-and-name (if path (str path " / " name) name)
|
||||||
|
gradient (:gradient color-item)
|
||||||
|
image (:image color-item)
|
||||||
|
color (:color color-item)]
|
||||||
|
|
||||||
(defn- gradient? [background]
|
(if (some? name)
|
||||||
(or
|
(cond
|
||||||
(str/starts-with? background "linear-gradient")
|
(some? color)
|
||||||
(str/starts-with? background "radial-gradient")))
|
(str/ffmt "% (%)" path-and-name color)
|
||||||
|
|
||||||
(defn- color-solid? [background]
|
(some? gradient)
|
||||||
(boolean
|
(str/ffmt "% (%)" path-and-name (uc/gradient-type->string (:type gradient)))
|
||||||
(or (re-matches hex-regex background)
|
|
||||||
(or (re-matches hsl-regex background)
|
|
||||||
(re-matches rgb-regex background)))))
|
|
||||||
|
|
||||||
(defn- color-opacity? [background]
|
(some? image)
|
||||||
(boolean
|
(str/ffmt "% (%)" path-and-name (tr "media.image"))
|
||||||
(or (re-matches hsla-regex background)
|
|
||||||
(re-matches rgba-regex background))))
|
|
||||||
|
|
||||||
(defn- extract-color-and-opacity [background]
|
:else
|
||||||
(cond
|
path-and-name)
|
||||||
(re-matches rgba-regex background)
|
|
||||||
(let [[_ r g b a] (re-matches rgba-regex background)]
|
|
||||||
{:color (dm/str "rgb(" r ", " g ", " b ")")
|
|
||||||
:opacity (js/parseFloat a)})
|
|
||||||
|
|
||||||
(re-matches hsla-regex background)
|
(cond
|
||||||
(let [[_ h s l a] (re-matches hsla-regex background)]
|
(some? color)
|
||||||
{:color (dm/str "hsl(" h ", " s "%, " l "%)")
|
color
|
||||||
:opacity (js/parseFloat a)})
|
|
||||||
|
|
||||||
:else
|
(some? gradient)
|
||||||
{:color background
|
(uc/gradient-type->string (:type gradient))
|
||||||
:opacity 1.0}))
|
|
||||||
|
(some? image)
|
||||||
|
(tr "media.image")))))
|
||||||
|
|
||||||
(mf/defc swatch*
|
(mf/defc swatch*
|
||||||
{::mf/props :obj
|
{::mf/props :obj
|
||||||
::mf/schema schema:swatch}
|
::mf/schema (sm/schema schema:swatch)}
|
||||||
[{:keys [background on-click format size active class]
|
[{:keys [background on-click size active class]
|
||||||
:rest props}]
|
:rest props}]
|
||||||
(let [element-type (if on-click "button" "div")
|
(let [background (if (object? background) (json/->clj background) background)
|
||||||
button-type (if on-click "button" nil)
|
read-only? (nil? on-click)
|
||||||
format (or format "square")
|
id? (some? (:id background))
|
||||||
|
element-type (if read-only? "div" "button")
|
||||||
|
button-type (if (not read-only?) "button" nil)
|
||||||
size (or size "small")
|
size (or size "small")
|
||||||
active (or active false)
|
active (or active false)
|
||||||
{:keys [color opacity]} (extract-color-and-opacity background)
|
gradient (:gradient background)
|
||||||
|
image (:image background)
|
||||||
|
format (if id? "rounded" "square")
|
||||||
class (dm/str class " " (stl/css-case
|
class (dm/str class " " (stl/css-case
|
||||||
:swatch true
|
:swatch true
|
||||||
:small (= size "small")
|
:small (= size "small")
|
||||||
|
@ -79,25 +86,26 @@
|
||||||
:active (= active true)
|
:active (= active true)
|
||||||
:interactive (= element-type "button")
|
:interactive (= element-type "button")
|
||||||
:rounded (= format "rounded")))
|
:rounded (= format "rounded")))
|
||||||
props (mf/spread-props props {:class class :on-click on-click :type button-type})]
|
props (mf/spread-props props {:class class
|
||||||
|
:on-click on-click
|
||||||
|
:type button-type
|
||||||
|
:title (color-title background)})]
|
||||||
|
|
||||||
[:> element-type props
|
[:> element-type props
|
||||||
(cond
|
(cond
|
||||||
(color-solid? background)
|
|
||||||
[:span {:class (stl/css :swatch-solid)
|
|
||||||
:style {:background background}}]
|
|
||||||
|
|
||||||
(color-opacity? background)
|
(some? gradient)
|
||||||
[:span {:class (stl/css :swatch-opacity)}
|
|
||||||
[:span {:class (stl/css :swatch-solid-side)
|
|
||||||
:style {:background color}}]
|
|
||||||
[:span {:class (stl/css :swatch-opacity-side)
|
|
||||||
:style {:background color :opacity opacity}}]]
|
|
||||||
|
|
||||||
(gradient? background)
|
|
||||||
[:span {:class (stl/css :swatch-gradient)
|
[:span {:class (stl/css :swatch-gradient)
|
||||||
:style {:background-image (str background ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
|
:style {:background-image (str gradient ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
|
||||||
|
|
||||||
|
(some? image)
|
||||||
|
(let [uri (cfg/resolve-file-media image)]
|
||||||
|
[:span {:class (stl/css :swatch-image)
|
||||||
|
:style {:background-image (str/ffmt "url(%)" uri)}}])
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[:span {:class (stl/css :swatch-image)
|
[:span {:class (stl/css :swatch-opacity)}
|
||||||
:style {:background-image (str "url('" background "'), repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}])]))
|
[:span {:class (stl/css :swatch-solid-side)
|
||||||
|
:style {:background (uc/color->background (assoc background :opacity 1))}}]
|
||||||
|
[:span {:class (stl/css :swatch-opacity-side)
|
||||||
|
:style {:background (uc/color->background background)}}]])]))
|
||||||
|
|
|
@ -7,56 +7,47 @@ import * as SwatchStories from "./swatch.stories";
|
||||||
|
|
||||||
Swatches are elements that display a color, gradient or image. They can sometimes trigger an action.
|
Swatches are elements that display a color, gradient or image. They can sometimes trigger an action.
|
||||||
|
|
||||||
|
## Background Property
|
||||||
|
|
||||||
|
A swatch component can receive several props. The `background` prop is the most important and must be an object. Depending on the value of the background property we will get different variants of the component.
|
||||||
|
|
||||||
## Variants
|
## Variants
|
||||||
|
|
||||||
**Color** (`"color"`), displays a solid color. It can take a hexadecimal, an rgb or an rgba.
|
If the background prop has a hex `color` value it will display a full swatch with a solid color
|
||||||
|
|
||||||
<Canvas of={SwatchStories.Default} />
|
<Canvas of={SwatchStories.Default} />
|
||||||
|
|
||||||
**WithOpacity** (`"color"`), displays a solid color on one side and the same color with its opacity applied on the other side. It can take a hexadecimal, an rgb or an rgba.
|
If the background prop has a hex `color` value and an opacity value it will display a full swatch with a solid color on one side and the same color with the opacity applied on the other side. (default opacity: 1)
|
||||||
|
|
||||||
<Canvas of={SwatchStories.WithOpacity} />
|
<Canvas of={SwatchStories.WithOpacity} />
|
||||||
|
|
||||||
**Gradient** (`"gradient"`), displays a gradient. A gradient should be a `linear-gradient` or a `conic-gradient`.
|
This component can take a size property to set the size of the swatch. In this case we can set it to `small` (default size: `medium`)
|
||||||
|
|
||||||
<Canvas of={SwatchStories.LinearGradient} />
|
|
||||||
|
|
||||||
**Image** (`"image"`) the swatch could display any image.
|
|
||||||
|
|
||||||
<Canvas of={SwatchStories.Image} />
|
|
||||||
|
|
||||||
**Active** (`"active"`) displays the swatch as active while an interface related action is happening.
|
|
||||||
|
|
||||||
<Canvas of={SwatchStories.Active} />
|
|
||||||
|
|
||||||
**Size** (`"size"`) shows a bigger or smaller swatch. Accepts `small` and `medium` (_default_) sizes.
|
|
||||||
|
|
||||||
<Canvas of={SwatchStories.Small} />
|
<Canvas of={SwatchStories.Small} />
|
||||||
|
|
||||||
**Format** (`"format"`) displays a square or rounded swatch. Accepts `square` (_default_) and `rounded` sizes.
|
With the `active` property, we can display the element as being active
|
||||||
|
|
||||||
<Canvas of={SwatchStories.Rounded} />
|
<Canvas of={SwatchStories.Active} />
|
||||||
|
|
||||||
|
The element can also be interactive, and execute an external function. Typically, it launches the color picker. To make it an interactive button, it accepts an onClick function.
|
||||||
|
|
||||||
|
<Canvas of={SwatchStories.Clickable} />
|
||||||
|
|
||||||
|
> Due to technical issues regarding the transformation between Clojurescript and Javascript, we are unable to display:
|
||||||
|
|
||||||
|
- Swatches with gradients
|
||||||
|
- Library Swatches
|
||||||
|
- Swatches with images
|
||||||
|
|
||||||
## Technical Notes
|
## Technical Notes
|
||||||
|
|
||||||
### Background
|
|
||||||
|
|
||||||
The `swatch*` component accepts a `background` prop, which must be:
|
|
||||||
|
|
||||||
- An hexadecimal (e.g. `#996633`)
|
|
||||||
- An RGB (e.g. `rgb(125, 125, 0)`)
|
|
||||||
- An RGBA (e.g. `rgba(125, 125, 0, 0.3)`)
|
|
||||||
- A linear gradient (e.g. `linear-gradient(to right, blue, pink)`)
|
|
||||||
- A conic gradient (e.g. `conic-gradient(red, orange, yellow, green, blue)`)
|
|
||||||
- An image (e.g. `url(https://placecats.com/100/100)`)
|
|
||||||
|
|
||||||
### onClick
|
### onClick
|
||||||
|
|
||||||
> Note: If the swatch is interactive, an `aria-label` is required. More on the `Accessibility` section.
|
> Note: If the swatch is interactive, an `aria-label` is required. See the `Accessibility` section for more information.
|
||||||
|
|
||||||
The swatch button accepts an onClick prop that expect a function on the parent context.
|
The swatch button accepts an onClick prop that expects a function on the parent context.
|
||||||
It should be useful for launching other tools as a color picker.
|
It should be useful for launching other tools as a color picker.
|
||||||
It runs when the user clics on the swatch, or presses enter or space while focusing it.
|
It is executed when the user clicks on the swatch, or presses Enter or Spacebar while focused.
|
||||||
|
|
||||||
### Accessibility
|
### Accessibility
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,7 @@ export default {
|
||||||
component: Swatch,
|
component: Swatch,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
background: {
|
background: {
|
||||||
control: { type: "text" },
|
control: "object",
|
||||||
},
|
|
||||||
format: {
|
|
||||||
control: "select",
|
|
||||||
options: ["square", "rounded"],
|
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
control: "select",
|
control: "select",
|
||||||
|
@ -30,8 +26,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
background: "#663399",
|
background: { color: "#7efff5" },
|
||||||
format: "square",
|
|
||||||
size: "medium",
|
size: "medium",
|
||||||
active: false,
|
active: false,
|
||||||
},
|
},
|
||||||
|
@ -42,28 +37,52 @@ export const Default = {};
|
||||||
|
|
||||||
export const WithOpacity = {
|
export const WithOpacity = {
|
||||||
args: {
|
args: {
|
||||||
background: "rgba(255, 0, 0, 0.5)",
|
background: {
|
||||||
|
color: "#7efff5",
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LinearGradient = {
|
// These stories are disabled because the gradient and the UUID variants cannot be translated from cljs into JS
|
||||||
args: {
|
// When the repo is updated to use the new version of rumext, these stories should be re-enabled and tested
|
||||||
background: "linear-gradient(to right, transparent, mistyrose)",
|
//
|
||||||
},
|
// export const LinearGradient = {
|
||||||
};
|
// args: {
|
||||||
|
// background: {
|
||||||
|
// gradient: {
|
||||||
|
// type: "linear",
|
||||||
|
// startX: 0,
|
||||||
|
// startY: 0,
|
||||||
|
// endX: 1,
|
||||||
|
// endY: 0,
|
||||||
|
// width: 1,
|
||||||
|
// stops: [
|
||||||
|
// {
|
||||||
|
// color: "#fabada",
|
||||||
|
// opacity: 1,
|
||||||
|
// offset: 0,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// color: "#cc0000",
|
||||||
|
// opacity: 0.5,
|
||||||
|
// offset: 1,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
export const Image = {
|
// export const Rounded = {
|
||||||
args: {
|
// args: {
|
||||||
background: "images/form/never-used.png",
|
// background: {
|
||||||
size: "medium",
|
// id: crypto.randomUUID(),
|
||||||
},
|
// color: "#7efff5",
|
||||||
};
|
// opacity: 0.5,
|
||||||
|
// },
|
||||||
export const Rounded = {
|
// },
|
||||||
args: {
|
// };
|
||||||
format: "rounded",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Small = {
|
export const Small = {
|
||||||
args: {
|
args: {
|
||||||
|
@ -74,7 +93,6 @@ export const Small = {
|
||||||
export const Active = {
|
export const Active = {
|
||||||
args: {
|
args: {
|
||||||
active: true,
|
active: true,
|
||||||
background: "#CC00CC",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue