0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-27 15:11:26 -05:00

Merge pull request #4919 from penpot/ladybenko-7869-ds-buttons

Implement design system buttons
This commit is contained in:
Andrey Antukh 2024-07-26 13:18:21 +02:00 committed by GitHub
commit e48aa909da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 494 additions and 1 deletions

View file

@ -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*

View file

@ -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);

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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]]))

View file

@ -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;
}

View file

@ -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",
},
};

View file

@ -0,0 +1,69 @@
import { Canvas, Meta } from '@storybook/blocks';
import * as ButtonStories from "./button.stories";
import * as IconButtonStories from "./icon_button.stories";
<Meta title="Buttons/Buttons" />
# Buttons
Buttons trigger an action such as submitting a form or showing/hiding an interface component.
## Variants
**Primary** (`"primary"`), used to initiate the main / primary action of a view or flow. Avoid to have more than one primary buttons in the same view or flow.
<Canvas of={ButtonStories.Primary} />
**Secondary** (`"secondary"`), the default and most common buttons in the interface. Use them for non primary actions.
<Canvas of={ButtonStories.Secondary} />
**Ghosts** (`"ghost"`), used for less prominent, and sometimes independent, actions (examples: add pages, add properties, etc.)
<Canvas of={ButtonStories.Ghost} />
**Destructive** (`"destructive"`), used for any action that destroys any object or data. Don't use them for actions like dettach, unlink, etc.
<Canvas of={ButtonStories.Destructive} />
## Technical notes
### Icons
Both `button*` and `icon-button*` accept an `icon` prop, which must contain an [icon ID](../foundations/assets/icon.mdx).
These are available in the `app.main.ds.foundations.assets.icon` namespace.
```clj
(ns app.main.ui.foo
(:require
[app.main.ui.ds.foundations.assets.icon :as i]))
```
```clj
[:> button* {:icon i/effects} "Lorem ipsum"]
```
<Canvas of={ButtonStories.WithIcon} />
### Accessibility
Icon buttons require an `aria-label`. This is also shown in a tooltip on hovering the button.
```clj
[:> icon-button* {:icon i/effects :aria-label "Lorem ipsum"}]
```
<Canvas of={IconButtonStories.Default} />
## Usage guidelines (design)
### Where to use
Penpot app has a high-density interface, so use buttons thoughtfully to establish
a clear and logical visual hierarchy, and avoid overwhelming the user.
### When to use
Buttons can be used in forms, navigation links or anywhere that needs simple,
standard button functionality. Used also to grab users' attention (i.e. navigate
to main user flows like register, etc.)

View file

@ -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}]]))

View file

@ -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;
}

View file

@ -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",
},
};