diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs index b15d52724..59895744a 100644 --- a/frontend/src/app/main/ui/ds.cljs +++ b/frontend/src/app/main/ui/ds.cljs @@ -6,6 +6,8 @@ (ns app.main.ui.ds (:require + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]] [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg* raw-svg-list]] [app.main.ui.ds.foundations.typography :refer [typography-list]] @@ -16,8 +18,10 @@ (def default "A export used for storybook" - #js {:Heading heading* + #js {:Button button* + :Heading heading* :Icon icon* + :IconButton icon-button* :Loader loader* :RawSvg raw-svg* :Text text* diff --git a/frontend/src/app/main/ui/ds/_borders.scss b/frontend/src/app/main/ui/ds/_borders.scss new file mode 100644 index 000000000..165ade57d --- /dev/null +++ b/frontend/src/app/main/ui/ds/_borders.scss @@ -0,0 +1,10 @@ +// 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 "./utils.scss" as *; + +// TODO: create actual tokens once we have them from design +$br-8: px2rem(8); diff --git a/frontend/src/app/main/ui/ds/_sizes.scss b/frontend/src/app/main/ui/ds/_sizes.scss new file mode 100644 index 000000000..f27838b6a --- /dev/null +++ b/frontend/src/app/main/ui/ds/_sizes.scss @@ -0,0 +1,10 @@ +// 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 "./utils.scss" as *; + +// TODO: create actual tokens once we have them from design +$sz-32: px2rem(32); diff --git a/frontend/src/app/main/ui/ds/buttons/_buttons.scss b/frontend/src/app/main/ui/ds/buttons/_buttons.scss new file mode 100644 index 000000000..7d8c896ac --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/_buttons.scss @@ -0,0 +1,132 @@ +// 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 "../utils.scss" as *; + +%base-button { + --button-bg-color: initial; + --button-fg-color: initial; + --button-hover-bg-color: initial; + --button-hover-fg-color: initial; + --button-active-bg-color: initial; + --button-disabled-bg-color: initial; + --button-disabled-fg-color: initial; + --button-border-color: var(--button-bg-color); + --button-focus-inner-ring-color: initial; + --button-focus-outer-ring-color: initial; + + appearance: none; + height: $sz-32; + border: none; + border-radius: $br-8; + + background: var(--button-bg-color); + color: var(--button-fg-color); + border: 1px solid var(--button-border-color); + + &:hover { + --button-bg-color: var(--button-hover-bg-color); + --button-fg-color: var(--button-hover-fg-color); + } + + &:active { + --button-bg-color: var(--button-active-bg-color); + } + + &:focus-visible { + outline: var(--button-focus-inner-ring-color) solid #{px2rem(2)}; + outline-offset: -#{px2rem(3)}; + --button-border-color: var(--button-focus-outer-ring-color); + --button-fg-color: var(--button-focus-fg-color); + } + + &:disabled { + --button-bg-color: var(--button-disabled-bg-color); + --button-fg-color: var(--button-disabled-fg-color); + } +} + +%base-button-primary { + --button-bg-color: var(--color-accent-primary); + --button-fg-color: var(--color-background-secondary); + + --button-hover-bg-color: var(--color-accent-tertiary); + --button-hover-fg-color: var(--color-background-secondary); + + --button-active-bg-color: var(--color-accent-tertiary); + + --button-disabled-bg-color: var(--color-accent-primary-muted); + --button-disabled-fg-color: var(--color-background-secondary); + + --button-focus-bg-color: var(--color-accent-primary); + --button-focus-fg-color: var(--color-background-secondary); + --button-focus-inner-ring-color: var(--color-background-secondary); + --button-focus-outer-ring-color: var(--color-accent-primary); + + &:active { + box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2); + } +} + +%base-button-secondary { + --button-bg-color: var(--color-background-tertiary); + --button-fg-color: var(--color-foreground-secondary); + + --button-hover-bg-color: var(--color-background-tertiary); + --button-hover-fg-color: var(--color-accent-primary); + + --button-active-bg-color: var(--color-background-quaternary); + + --button-disabled-bg-color: transparent; + --button-disabled-fg-color: var(--color-foreground-secondary); + + --button-focus-bg-color: var(--color-background-tertiary); + --button-focus-fg-color: var(--color-foreground-primary); + --button-focus-inner-ring-color: var(--color-background-secondary); + --button-focus-outer-ring-color: var(--color-accent-primary); +} + +%base-button-ghost { + --button-bg-color: transparent; + --button-fg-color: var(--color-foreground-secondary); + + --button-hover-bg-color: var(--color-background-tertiary); + --button-hover-fg-color: var(--color-accent-primary); + + --button-active-bg-color: var(--color-background-quaternary); + + --button-disabled-bg-color: transparent; + --button-disabled-fg-color: var(--color-accent-primary-muted); + + --button-focus-bg-color: transparent; + --button-focus-fg-color: var(--color-foreground-secondary); + --button-focus-inner-ring-color: transparent; + --button-focus-outer-ring-color: var(--color-accent-primary); +} + +%base-button-destructive { + --button-bg-color: var(--color-accent-error); + --button-fg-color: var(--color-foreground-primary); + + --button-hover-bg-color: var(--color-background-error); + --button-hover-fg-color: var(--color-foreground-primary); + + --button-active-bg-color: var(--color-accent-error); + + --button-disabled-bg-color: var(--color-background-error); + --button-disabled-fg-color: var(--color-accent-error); + + --button-focus-bg-color: var(--color-accent-error); + --button-focus-fg-color: var(--color-foreground-primary); + --button-focus-inner-ring-color: var(--color-background-primary); + --button-focus-outer-ring-color: var(--color-accent-primary); + + &:active { + box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2); + } +} diff --git a/frontend/src/app/main/ui/ds/buttons/button.cljs b/frontend/src/app/main/ui/ds/buttons/button.cljs new file mode 100644 index 000000000..8086758bb --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/button.cljs @@ -0,0 +1,30 @@ +;; 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.buttons.button + (:require-macros + [app.common.data.macros :as dm] + [app.main.style :as stl]) + (:require + [app.main.ui.ds.foundations.assets.icon :refer [icon*]] + [rumext.v2 :as mf])) + +(def button-variants (set '("primary" "secondary" "ghost" "destructive"))) + +(mf/defc button* + {::mf/props :obj} + [{:keys [variant icon children class] :rest props}] + (assert (or (nil? variant) (contains? button-variants variant) "expected valid variant")) + (let [variant (or variant "primary") + class (dm/str class " " (stl/css-case :button true + :button-primary (= variant "primary") + :button-secondary (= variant "secondary") + :button-ghost (= variant "ghost") + :button-destructive (= variant "destructive"))) + props (mf/spread-props props {:class class})] + [:> "button" props + (when icon [:> icon* {:id icon :size "m"}]) + [:span {:class (stl/css :label-wrapper)} children]])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/ds/buttons/button.scss b/frontend/src/app/main/ui/ds/buttons/button.scss new file mode 100644 index 000000000..5e7b2cfe6 --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/button.scss @@ -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 + +@use "../typography.scss" as *; +@use "./buttons" as *; + +.button { + @extend %base-button; + + @include use-typography("headline-small"); + padding: 0 var(--sp-m); + + display: inline-flex; + align-items: center; + column-gap: var(--sp-xs); +} + +.button-primary { + @extend %base-button-primary; +} + +.button-secondary { + @extend %base-button-secondary; +} + +.button-ghost { + @extend %base-button-ghost; +} + +.button-destructive { + @extend %base-button-destructive; +} diff --git a/frontend/src/app/main/ui/ds/buttons/button.stories.jsx b/frontend/src/app/main/ui/ds/buttons/button.stories.jsx new file mode 100644 index 000000000..8a2c78a15 --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/button.stories.jsx @@ -0,0 +1,74 @@ +// 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"; + +const { Button } = Components; +const { icons } = Components.meta; + +const iconList = [ + ...Object.entries(icons) + .map(([_, value]) => value) + .sort(), +]; + +export default { + title: "Buttons/Button", + component: Components.Button, + argTypes: { + icon: { + options: iconList, + control: { type: "select" }, + }, + disabled: { control: "boolean" }, + variant: { + options: ["primary", "secondary", "ghost", "destructive"], + control: { type: "select" }, + }, + }, + args: { + children: "Lorem ipsum", + disabled: false, + variant: undefined, + }, + parameters: { + controls: { exclude: ["children"] }, + }, + render: ({ ...args }) => <Button {...args} />, +}; + +export const Default = {}; + +export const WithIcon = { + args: { + icon: "effects", + }, +}; + +export const Primary = { + args: { + variant: "primary", + }, +}; + +export const Secondary = { + args: { + variant: "secondary", + }, +}; + +export const Ghost = { + args: { + variant: "ghost", + }, +}; + +export const Destructive = { + args: { + variant: "destructive", + }, +}; diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs new file mode 100644 index 000000000..addfc6372 --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs @@ -0,0 +1,30 @@ +;; 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.buttons.icon-button + (:require-macros + [app.common.data.macros :as dm] + [app.main.style :as stl]) + (:require + [app.main.ui.ds.foundations.assets.icon :refer [icon*]] + [rumext.v2 :as mf])) + +(def button-variants (set '("primary" "secondary" "ghost" "destructive"))) + +(mf/defc icon-button* + {::mf/props :obj} + [{:keys [class icon variant aria-label] :rest props}] + (assert (or (not variant) (contains? button-variants variant)) "invalid variant") + (assert (some? aria-label) "aria-label must be provided") + (assert (some? icon) "an icon id must be provided") + (let [variant (or variant "primary") + class (dm/str class " " (stl/css-case :icon-button true + :icon-button-primary (= variant "primary") + :icon-button-secondary (= variant "secondary") + :icon-button-ghost (= variant "ghost") + :icon-button-destructive (= variant "destructive"))) + props (mf/spread-props props {:class class :title aria-label})] + [:> "button" props [:> icon* {:id icon :aria-label aria-label}]])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.scss b/frontend/src/app/main/ui/ds/buttons/icon_button.scss new file mode 100644 index 000000000..1a10c3775 --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/icon_button.scss @@ -0,0 +1,33 @@ +// 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 "../_sizes.scss" as *; +@use "./buttons" as *; + +.icon-button { + @extend %base-button; + width: #{$sz-32}; + display: flex; + justify-content: center; + align-items: center; +} + +.icon-button-primary { + @extend %base-button-primary; +} + +.icon-button-secondary { + @extend %base-button-secondary; +} + +.icon-button-ghost { + @extend %base-button-ghost; +} + +.icon-button-destructive { + @extend %base-button-destructive; +} diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.stories.jsx b/frontend/src/app/main/ui/ds/buttons/icon_button.stories.jsx new file mode 100644 index 000000000..17cb4b2fb --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/icon_button.stories.jsx @@ -0,0 +1,66 @@ +// 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"; + +const { IconButton } = Components; +const { icons } = Components.meta; + +const iconList = [ + ...Object.entries(icons) + .map(([_, value]) => value) + .sort(), +]; + +export default { + title: "Buttons/IconButton", + component: Components.IconButton, + argTypes: { + icon: { + options: iconList, + control: { type: "select" }, + }, + disabled: { control: "boolean" }, + variant: { + options: ["primary", "secondary", "ghost", "destructive"], + control: { type: "select" }, + }, + }, + args: { + disabled: false, + variant: undefined, + "aria-label": "Lorem ipsum", + icon: "effects", + }, + render: ({ ...args }) => <IconButton {...args} />, +}; + +export const Default = {}; + +export const Primary = { + args: { + variant: "primary", + }, +}; + +export const Secondary = { + args: { + variant: "secondary", + }, +}; + +export const Ghost = { + args: { + variant: "ghost", + }, +}; + +export const Destructive = { + args: { + variant: "destructive", + }, +};