mirror of
https://github.com/penpot/penpot.git
synced 2025-02-01 11:59:17 -05:00
Merge pull request #5402 from penpot/xaviju-9414-ds-swatch
Create DS swatch utility component
This commit is contained in:
commit
62fcf74472
6 changed files with 363 additions and 0 deletions
|
@ -21,6 +21,7 @@
|
||||||
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
|
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
|
||||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||||
[app.main.ui.ds.storybook :as sb]
|
[app.main.ui.ds.storybook :as sb]
|
||||||
|
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
|
||||||
[app.util.i18n :as i18n]))
|
[app.util.i18n :as i18n]))
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
:Text text*
|
:Text text*
|
||||||
:TabSwitcher tab-switcher*
|
:TabSwitcher tab-switcher*
|
||||||
:Toast toast*
|
:Toast toast*
|
||||||
|
:Swatch swatch*
|
||||||
;; meta / misc
|
;; meta / misc
|
||||||
:meta #js {:icons (clj->js (sort icon-list))
|
:meta #js {:icons (clj->js (sort icon-list))
|
||||||
:svgs (clj->js (sort raw-svg-list))
|
:svgs (clj->js (sort raw-svg-list))
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
// TODO: create actual tokens once we have them from design
|
// TODO: create actual tokens once we have them from design
|
||||||
$sz-16: px2rem(16);
|
$sz-16: px2rem(16);
|
||||||
|
$sz-24: px2rem(24);
|
||||||
$sz-32: px2rem(32);
|
$sz-32: px2rem(32);
|
||||||
$sz-36: px2rem(36);
|
$sz-36: px2rem(36);
|
||||||
$sz-160: px2rem(160);
|
$sz-160: px2rem(160);
|
||||||
|
|
103
frontend/src/app/main/ui/ds/utilities/swatch.cljs
Normal file
103
frontend/src/app/main/ui/ds/utilities/swatch.cljs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
|
||||||
|
;; 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.utilities.swatch
|
||||||
|
(:require-macros
|
||||||
|
[app.main.style :as stl])
|
||||||
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(def ^:private schema:swatch
|
||||||
|
[:map
|
||||||
|
[:background :string]
|
||||||
|
[: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- gradient? [background]
|
||||||
|
(or
|
||||||
|
(str/starts-with? background "linear-gradient")
|
||||||
|
(str/starts-with? background "radial-gradient")))
|
||||||
|
|
||||||
|
(defn- color-solid? [background]
|
||||||
|
(boolean
|
||||||
|
(or (re-matches hex-regex background)
|
||||||
|
(or (re-matches hsl-regex background)
|
||||||
|
(re-matches rgb-regex background)))))
|
||||||
|
|
||||||
|
(defn- color-opacity? [background]
|
||||||
|
(boolean
|
||||||
|
(or (re-matches hsla-regex background)
|
||||||
|
(re-matches rgba-regex background))))
|
||||||
|
|
||||||
|
(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)})
|
||||||
|
|
||||||
|
(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)})
|
||||||
|
|
||||||
|
:else
|
||||||
|
{:color background
|
||||||
|
:opacity 1.0}))
|
||||||
|
|
||||||
|
(mf/defc swatch*
|
||||||
|
{::mf/props :obj
|
||||||
|
::mf/schema schema:swatch}
|
||||||
|
[{:keys [background on-click format size active class]
|
||||||
|
:rest props}]
|
||||||
|
(let [element-type (if on-click "button" "div")
|
||||||
|
button-type (if on-click "button" nil)
|
||||||
|
format (or format "square")
|
||||||
|
size (or size "small")
|
||||||
|
active (or active false)
|
||||||
|
{:keys [color opacity]} (extract-color-and-opacity background)
|
||||||
|
class (dm/str class " " (stl/css-case
|
||||||
|
:swatch true
|
||||||
|
:small (= size "small")
|
||||||
|
:medium (= size "medium")
|
||||||
|
:square (= format "square")
|
||||||
|
:active (= active true)
|
||||||
|
:interactive (= element-type "button")
|
||||||
|
:rounded (= format "rounded")))
|
||||||
|
props (mf/spread-props props {:class class :on-click on-click :type button-type})]
|
||||||
|
|
||||||
|
[:> 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)
|
||||||
|
[:span {:class (stl/css :swatch-gradient)
|
||||||
|
:style {:background-image (str background ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
|
||||||
|
|
||||||
|
:else
|
||||||
|
[:span {:class (stl/css :swatch-image)
|
||||||
|
:style {:background-image (str "url('" background "'), repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}])]))
|
67
frontend/src/app/main/ui/ds/utilities/swatch.mdx
Normal file
67
frontend/src/app/main/ui/ds/utilities/swatch.mdx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { Canvas, Meta } from "@storybook/blocks";
|
||||||
|
import * as SwatchStories from "./swatch.stories";
|
||||||
|
|
||||||
|
<Meta title="Foundations/Utilities/Swatch/Docs" />
|
||||||
|
|
||||||
|
# Swatch
|
||||||
|
|
||||||
|
Swatches are elements that display a color, gradient or image. They can sometimes trigger an action.
|
||||||
|
|
||||||
|
## Variants
|
||||||
|
|
||||||
|
**Color** (`"color"`), displays a solid color. It can take a hexadecimal, an rgb or an rgba.
|
||||||
|
|
||||||
|
<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.
|
||||||
|
|
||||||
|
<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.
|
||||||
|
|
||||||
|
<Canvas of={SwatchStories.Small} />
|
||||||
|
|
||||||
|
**Format** (`"format"`) displays a square or rounded swatch. Accepts `square` (_default_) and `rounded` sizes.
|
||||||
|
|
||||||
|
<Canvas of={SwatchStories.Rounded} />
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
The swatch button accepts an onClick prop that expect 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.
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
If the swatch is interactive, an `aria-label` is required.
|
||||||
|
|
||||||
|
```clj
|
||||||
|
[:> swatch* {:on-click launch-colorpicker :aria-label "Lorem ipsum"}]
|
||||||
|
```
|
104
frontend/src/app/main/ui/ds/utilities/swatch.scss
Normal file
104
frontend/src/app/main/ui/ds/utilities/swatch.scss
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
// 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 "../colors.scss" as *;
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
--border-color: var(--color-accent-primary-muted);
|
||||||
|
--border-radius: #{$br-4};
|
||||||
|
--border-color-active: var(--color-foreground-primary);
|
||||||
|
--border-color-active-inset: var(--color-background-primary);
|
||||||
|
|
||||||
|
--checkerboard-background: repeating-conic-gradient(lightgray 0% 25%, white 0% 50%);
|
||||||
|
--checkerboard-size: 0.5rem 0.5rem;
|
||||||
|
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
--border-color: var(--color-accent-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
inline-size: $sz-16;
|
||||||
|
block-size: $sz-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
--checkerboard-size: 1rem 1rem;
|
||||||
|
|
||||||
|
inline-size: $sz-24;
|
||||||
|
block-size: $sz-24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded {
|
||||||
|
--border-radius: #{$br-circle};
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
--border-color: var(--border-color-active);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: 0;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 0 0 1px var(--border-color-active-inset) inset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch-image,
|
||||||
|
.swatch-gradient,
|
||||||
|
.swatch-opacity,
|
||||||
|
.swatch-solid {
|
||||||
|
block-size: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch-gradient {
|
||||||
|
background-size: cover, var(--checkerboard-size);
|
||||||
|
background-position: center, center;
|
||||||
|
background-repeat: no-repeat, repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch-image {
|
||||||
|
background-size: cover, var(--checkerboard-size);
|
||||||
|
background-position: center, center;
|
||||||
|
background-repeat: no-repeat, repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch-opacity {
|
||||||
|
background: var(--checkerboard-background);
|
||||||
|
background-size: var(--checkerboard-size);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch-solid-side,
|
||||||
|
.swatch-opacity-side {
|
||||||
|
flex: 1;
|
||||||
|
display: block;
|
||||||
|
}
|
86
frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx
Normal file
86
frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import Components from "@target/components";
|
||||||
|
import { action } from "@storybook/addon-actions";
|
||||||
|
|
||||||
|
const { Swatch } = Components;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Foundations/Utilities/Swatch",
|
||||||
|
component: Swatch,
|
||||||
|
argTypes: {
|
||||||
|
background: {
|
||||||
|
control: { type: "text" },
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
control: "select",
|
||||||
|
options: ["square", "rounded"],
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: "select",
|
||||||
|
options: ["small", "medium"],
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
control: { type: "boolean" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
background: "#663399",
|
||||||
|
format: "square",
|
||||||
|
size: "medium",
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
render: ({ ...args }) => <Swatch {...args} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = {};
|
||||||
|
|
||||||
|
export const WithOpacity = {
|
||||||
|
args: {
|
||||||
|
background: "rgba(255, 0, 0, 0.5)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LinearGradient = {
|
||||||
|
args: {
|
||||||
|
background: "linear-gradient(to right, transparent, mistyrose)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Image = {
|
||||||
|
args: {
|
||||||
|
background: "images/form/never-used.png",
|
||||||
|
size: "medium",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Rounded = {
|
||||||
|
args: {
|
||||||
|
format: "rounded",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Small = {
|
||||||
|
args: {
|
||||||
|
size: "small",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Active = {
|
||||||
|
args: {
|
||||||
|
active: true,
|
||||||
|
background: "#CC00CC",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Clickable = {
|
||||||
|
args: {
|
||||||
|
onClick: action("on-click"),
|
||||||
|
"aria-label": "Click swatch",
|
||||||
|
},
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue