0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-20 05:34:23 -05:00

♻️ refactor swatch component

This commit is contained in:
Xaviju 2025-01-07 12:28:41 +01:00 committed by Xaviju
parent 0c80bf76b8
commit d980ff05cd
3 changed files with 129 additions and 112 deletions

View file

@ -7,70 +7,77 @@
(ns app.main.ui.ds.utilities.swatch
(:require-macros
[app.main.style :as stl])
(:require
[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]
[rumext.v2 :as mf]))
(def ^:private schema:swatch
[:map
[:background :string]
[:map {:title "SchemaSwatch"}
[:background {:optional true} ct/schema:color]
[:class {:optional true} :string]
[:format {:optional true} [:enum "square" "rounded"]]
[:size {:optional true} [:enum "small" "medium"]]
[:active {:optional true} :boolean]
[:on-click {:optional true} fn?]])
(def hex-regex #"^#(?:[0-9a-fA-F]{3}){1,2}$")
(def rgb-regex #"^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$")
(def hsl-regex #"^hsl\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%\)$")
(def hsla-regex #"^hsla\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%,\s*(0|1|0?\.\d+)\)$")
(def rgba-regex #"^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(0|1|0?\.\d+)\)$")
(defn- color-title
[color-item]
(let [name (:name color-item)
path (:path color-item)
path-and-name (if path (str path " / " name) name)
gradient (:gradient color-item)
image (:image color-item)
color (:color color-item)]
(defn- gradient? [background]
(or
(str/starts-with? background "linear-gradient")
(str/starts-with? background "radial-gradient")))
(if (some? name)
(cond
(some? color)
(str/ffmt "% (%)" path-and-name color)
(defn- color-solid? [background]
(boolean
(or (re-matches hex-regex background)
(or (re-matches hsl-regex background)
(re-matches rgb-regex background)))))
(some? gradient)
(str/ffmt "% (%)" path-and-name (uc/gradient-type->string (:type gradient)))
(defn- color-opacity? [background]
(boolean
(or (re-matches hsla-regex background)
(re-matches rgba-regex background))))
(some? image)
(str/ffmt "% (%)" path-and-name (tr "media.image"))
(defn- extract-color-and-opacity [background]
(cond
(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)})
:else
path-and-name)
(re-matches hsla-regex background)
(let [[_ h s l a] (re-matches hsla-regex background)]
{:color (dm/str "hsl(" h ", " s "%, " l "%)")
:opacity (js/parseFloat a)})
(cond
(some? color)
color
:else
{:color background
:opacity 1.0}))
(some? gradient)
(uc/gradient-type->string (:type gradient))
(some? image)
(tr "media.image")))))
(mf/defc swatch*
{::mf/props :obj
::mf/schema schema:swatch}
[{:keys [background on-click format size active class]
::mf/schema (sm/schema schema:swatch)}
[{:keys [background on-click size active class]
:rest props}]
(let [element-type (if on-click "button" "div")
button-type (if on-click "button" nil)
format (or format "square")
(let [background (if (object? background) (json/->clj background) background)
read-only? (nil? on-click)
id? (some? (:id background))
element-type (if read-only? "div" "button")
button-type (if (not read-only?) "button" nil)
size (or size "small")
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
:swatch true
:small (= size "small")
@ -79,25 +86,26 @@
:active (= active true)
:interactive (= element-type "button")
: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
(cond
(color-solid? background)
[:span {:class (stl/css :swatch-solid)
:style {:background background}}]
(color-opacity? background)
[: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)
(some? 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
[:span {:class (stl/css :swatch-image)
:style {:background-image (str "url('" background "'), repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}])]))
[:span {:class (stl/css :swatch-opacity)}
[: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)}}]])]))

View file

@ -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.
## 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
**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} />
**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} />
**Gradient** (`"gradient"`), displays a gradient. A gradient should be a `linear-gradient` or a `conic-gradient`.
<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.
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.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
### 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
> 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 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

View file

@ -15,11 +15,7 @@ export default {
component: Swatch,
argTypes: {
background: {
control: { type: "text" },
},
format: {
control: "select",
options: ["square", "rounded"],
control: "object",
},
size: {
control: "select",
@ -30,8 +26,7 @@ export default {
},
},
args: {
background: "#663399",
format: "square",
background: { color: "#7efff5" },
size: "medium",
active: false,
},
@ -42,28 +37,52 @@ export const Default = {};
export const WithOpacity = {
args: {
background: "rgba(255, 0, 0, 0.5)",
background: {
color: "#7efff5",
opacity: 0.5,
},
},
};
export const LinearGradient = {
args: {
background: "linear-gradient(to right, transparent, mistyrose)",
},
};
// These stories are disabled because the gradient and the UUID variants cannot be translated from cljs into JS
// When the repo is updated to use the new version of rumext, these stories should be re-enabled and tested
//
// 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 = {
args: {
background: "images/form/never-used.png",
size: "medium",
},
};
export const Rounded = {
args: {
format: "rounded",
},
};
// export const Rounded = {
// args: {
// background: {
// id: crypto.randomUUID(),
// color: "#7efff5",
// opacity: 0.5,
// },
// },
// };
export const Small = {
args: {
@ -74,7 +93,6 @@ export const Small = {
export const Active = {
args: {
active: true,
background: "#CC00CC",
},
};