From 89009c4da1d8f2d871b6ff99fa678f08c9c28c59 Mon Sep 17 00:00:00 2001 From: Xaviju Date: Tue, 26 Nov 2024 15:40:14 +0100 Subject: [PATCH] :sparkles: Create Ds swatch utility component --- frontend/src/app/main/ui/ds.cljs | 2 + frontend/src/app/main/ui/ds/_sizes.scss | 1 + .../src/app/main/ui/ds/utilities/swatch.cljs | 103 +++++++++++++++++ .../src/app/main/ui/ds/utilities/swatch.mdx | 67 +++++++++++ .../src/app/main/ui/ds/utilities/swatch.scss | 104 ++++++++++++++++++ .../main/ui/ds/utilities/swatch.stories.jsx | 86 +++++++++++++++ 6 files changed, 363 insertions(+) create mode 100644 frontend/src/app/main/ui/ds/utilities/swatch.cljs create mode 100644 frontend/src/app/main/ui/ds/utilities/swatch.mdx create mode 100644 frontend/src/app/main/ui/ds/utilities/swatch.scss create mode 100644 frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs index 89f8a4961..7cb60c1ca 100644 --- a/frontend/src/app/main/ui/ds.cljs +++ b/frontend/src/app/main/ui/ds.cljs @@ -21,6 +21,7 @@ [app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]] [app.main.ui.ds.product.loader :refer [loader*]] [app.main.ui.ds.storybook :as sb] + [app.main.ui.ds.utilities.swatch :refer [swatch*]] [app.util.i18n :as i18n])) @@ -40,6 +41,7 @@ :Text text* :TabSwitcher tab-switcher* :Toast toast* + :Swatch swatch* ;; meta / misc :meta #js {:icons (clj->js (sort icon-list)) :svgs (clj->js (sort raw-svg-list)) diff --git a/frontend/src/app/main/ui/ds/_sizes.scss b/frontend/src/app/main/ui/ds/_sizes.scss index 1011d1285..5301f6b44 100644 --- a/frontend/src/app/main/ui/ds/_sizes.scss +++ b/frontend/src/app/main/ui/ds/_sizes.scss @@ -8,6 +8,7 @@ // TODO: create actual tokens once we have them from design $sz-16: px2rem(16); +$sz-24: px2rem(24); $sz-32: px2rem(32); $sz-36: px2rem(36); $sz-160: px2rem(160); diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.cljs b/frontend/src/app/main/ui/ds/utilities/swatch.cljs new file mode 100644 index 000000000..bfce203c8 --- /dev/null +++ b/frontend/src/app/main/ui/ds/utilities/swatch.cljs @@ -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%)")}}])])) diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.mdx b/frontend/src/app/main/ui/ds/utilities/swatch.mdx new file mode 100644 index 000000000..a091a4e32 --- /dev/null +++ b/frontend/src/app/main/ui/ds/utilities/swatch.mdx @@ -0,0 +1,67 @@ +import { Canvas, Meta } from "@storybook/blocks"; +import * as SwatchStories from "./swatch.stories"; + + + +# 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. + + + +**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. + + + +**Gradient** (`"gradient"`), displays a gradient. A gradient should be a `linear-gradient` or a `conic-gradient`. + + + +**Image** (`"image"`) the swatch could display any image. + + + +**Active** (`"active"`) displays the swatch as active while an interface related action is happening. + + + +**Size** (`"size"`) shows a bigger or smaller swatch. Accepts `small` and `medium` (_default_) sizes. + + + +**Format** (`"format"`) displays a square or rounded swatch. Accepts `square` (_default_) and `rounded` sizes. + + + +## 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"}] +``` diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.scss b/frontend/src/app/main/ui/ds/utilities/swatch.scss new file mode 100644 index 000000000..938bfc732 --- /dev/null +++ b/frontend/src/app/main/ui/ds/utilities/swatch.scss @@ -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; +} diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx b/frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx new file mode 100644 index 000000000..165b7c599 --- /dev/null +++ b/frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx @@ -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 }) => , +}; + +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", + }, +};