🎉 Create new empty-state component (#7903)

This commit is contained in:
Luis de Dios
2025-12-09 16:48:12 +01:00
committed by GitHub
parent 1798461d21
commit e35fc85c3d
20 changed files with 202 additions and 200 deletions

View File

@@ -746,20 +746,6 @@
}
}
.empty-icon {
@include flexCenter;
height: $s-48;
width: $s-48;
border-radius: $br-circle;
background-color: var(--empty-message-background-color);
svg {
@extend .button-icon;
height: $s-28;
width: $s-28;
stroke: var(--empty-message-foreground-color);
}
}
.attr-title {
div {
margin-left: 0;

View File

@@ -32,6 +32,7 @@
[app.main.ui.ds.product.avatar :refer [avatar*]]
[app.main.ui.ds.product.cta :refer [cta*]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.ds.product.milestone :refer [milestone*]]
@@ -56,6 +57,7 @@
:HintMessage hint-message*
:InputWithMeta input-with-meta*
:EmptyPlaceholder empty-placeholder*
:EmptyState empty-state*
:Loader loader*
:RawSvg raw-svg*
:Select select*

View File

@@ -0,0 +1,29 @@
;; 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.product.empty-state
(:require-macros
[app.main.style :as stl])
(:require
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[rumext.v2 :as mf]))
(def ^:private schema:empty-state
[:map
[:class {:optional true} :string]
[:icon [:and :string [:fn #(contains? icon-list %)]]]
[:text :string]])
(mf/defc empty-state*
{::mf/schema schema:empty-state}
[{:keys [class icon text] :rest props}]
(let [props (mf/spread-props props {:class [class (stl/css :group)]})]
[:> :div props
[:div {:class (stl/css :icon-wrapper)}
[:> icon* {:icon-id icon
:size "l"
:class (stl/css :icon)}]]
[:div {:class (stl/css :text)} text]]))

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 */ }
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as EmptyState from "./empty_state.stories";
<Meta title="Product/EmptyState" />
# EmptyState
An indication to the user that there is no data to display in the current view; often includes instructions on what to do next.
<Canvas of={EmptyState.Default} />
## Technical notes
The icon and the text are mandatory, and a class is optional.
```clj
[:> empty-state* {:icon i/at
:text "This is an empty state"}]
```
## Usage guidelines (design)
### Where to use
Use in areas of the app where users can add data or elements, but where there is currently no available information.
### Interaction / Behavior
There is no interaction.

View File

@@ -0,0 +1,36 @@
// 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 "ds/_sizes.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/typography.scss" as t;
.group {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-m);
}
.icon-wrapper {
display: flex;
justify-content: center;
align-items: center;
inline-size: $sz-48;
block-size: $sz-48;
}
.icon {
color: var(--color-foreground-secondary);
inline-size: $sz-32;
block-size: $sz-32;
}
.text {
@include t.use-typography("body-small");
text-align: center;
color: var(--color-foreground-secondary);
}

View File

@@ -0,0 +1,32 @@
// 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 { EmptyState } = Components;
const { icons } = Components.meta;
export default {
title: "Product/EmptyState",
component: Components.EmptyState,
argTypes: {
icon: {
options: icons,
control: { type: "select" },
},
text: {
control: { type: "text" },
},
},
args: {
icon: "help",
text: "This is an empty state",
},
render: ({ ...args }) => <EmptyState {...args} />,
};
export const Default = {};

View File

@@ -12,10 +12,11 @@
[app.main.data.event :as ev]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.inspect.attributes :refer [attributes]]
[app.main.ui.inspect.code :refer [code]]
[app.main.ui.inspect.selection-feedback :refer [resolve-shapes]]
@@ -224,17 +225,14 @@
:shapes shapes
:on-expand handle-expand
:from from}])])]]
[:div {:class (stl/css :empty)}
[:div {:class (stl/css :code-info)}
[:span {:class (stl/css :placeholder-icon)}
deprecated-icon/code]
[:span {:class (stl/css :placeholder-label)}
(tr "inspect.empty.select")]]
[:div {:class (stl/css :help-info)}
[:span {:class (stl/css :placeholder-icon)}
deprecated-icon/help]
[:span {:class (stl/css :placeholder-label)}
(tr "inspect.empty.help")]]
[:button {:class (stl/css :more-info-btn)
:on-click navigate-to-help}
(tr "inspect.empty.more-info")]])]))
[:*
[:div {:class (stl/css :empty)}
[:> empty-state* {:icon i/code
:text (tr "inspect.empty.select")}]
[:> empty-state* {:icon i/help
:text (tr "inspect.empty.help")}]]
[:div {:class (stl/css :empty-button)}
[:> button* {:variant "secondary"
:on-click navigate-to-help}
(tr "inspect.empty.more")]]])]))

View File

@@ -81,36 +81,14 @@
.empty {
display: flex;
flex-direction: column;
align-items: center;
gap: $sz-40;
padding-top: $sz-24;
gap: var(--sp-xxxl);
margin: var(--sp-xxxl) auto;
inline-size: $sz-200;
}
.code-info,
.help-info {
@include deprecated.flexColumn;
align-items: center;
justify-content: flex-start;
gap: var(--sp-m);
margin-inline-end: var(--sp-s);
}
.placeholder-icon {
@extend .empty-icon;
}
.placeholder-label {
@include deprecated.bodySmallTypography;
text-align: center;
inline-size: px2rem(200);
color: var(--empty-message-foreground-color);
}
.more-info-btn {
@extend .button-secondary;
@include deprecated.uppercaseTitleTipography;
block-size: $sz-32;
padding: var(--sp-s) var(--sp-xxl);
.empty-button {
display: flex;
justify-content: center;
}
.inspect-tab-switcher {

View File

@@ -18,6 +18,7 @@
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -160,6 +161,5 @@
:key (:page-id tgroup)}])]
[:div {:class (stl/css :thread-group-placeholder)}
[:span {:class (stl/css :placeholder-icon)} deprecated-icon/comments]
[:span {:class (stl/css :placeholder-label)}
(tr "labels.no-comments-available")]])]]))
[:> empty-state* {:icon i/comments
:text (tr "labels.no-comments-available")}]])]]))

View File

@@ -4,6 +4,7 @@
//
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "refactor/common-refactor.scss" as deprecated;
.comments-section {
@@ -128,29 +129,9 @@
}
.thread-group-placeholder {
@include deprecated.flexColumn;
align-items: center;
justify-content: flex-start;
margin-top: deprecated.$s-36;
}
.placeholder-icon {
@include deprecated.flexCenter;
height: deprecated.$s-48;
width: deprecated.$s-48;
border-radius: deprecated.$br-circle;
background-color: var(--empty-message-background-color);
svg {
@extend .button-icon;
height: deprecated.$s-28;
width: deprecated.$s-28;
stroke: var(--empty-message-foreground-color);
}
}
.placeholder-label {
@include deprecated.bodySmallTypography;
text-align: center;
width: deprecated.$s-184;
color: var(--empty-message-foreground-color);
display: flex;
flex-direction: column;
gap: var(--sp-xxxl);
margin: var(--sp-xxxl) auto;
inline-size: $sz-200;
}

View File

@@ -33,6 +33,7 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
[app.util.color :as uc]
@@ -49,9 +50,6 @@
(def ^:private add-icon
(deprecated-icon/icon-xref :add (stl/css :add-icon)))
(def ^:private library-icon
(deprecated-icon/icon-xref :library (stl/css :library-icon)))
(defn- get-library-summary
"Given a library data return a summary representation of this library"
[data]
@@ -493,9 +491,8 @@
[:div {:class (stl/css :update-section)}
(if (empty? libs-assets)
[:div {:class (stl/css :section-list-empty)}
[:span {:class (stl/css :empty-state-icon)}
library-icon]
(tr "workspace.libraries.no-libraries-need-sync")]
[:> empty-state* {:icon i/library
:text (tr "workspace.libraries.no-libraries-need-sync")}]]
[:*
[:div {:class (stl/css :section-title)} (tr "workspace.libraries.library-updates")]

View File

@@ -192,7 +192,6 @@
// empty state
.section-list-empty {
@include t.use-typography("body-medium");
display: grid;
grid-template-rows: auto 1fr;
justify-items: center;
@@ -200,17 +199,6 @@
text-align: center;
height: fit-content;
margin-block: var(--sp-l);
color: var(--modal-title-foreground-color);
}
.empty-state-icon {
display: flex;
justify-content: center;
align-items: center;
width: $sz-48;
height: $sz-48;
border-radius: $br-circle;
background-color: var(--pill-background-color);
}
.library-icon {

View File

@@ -12,6 +12,8 @@
[app.main.data.workspace.undo :as dwu]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :refer [tr] :as i18n]
@@ -327,8 +329,8 @@
[:div {:class (stl/css :history-toolbox)}
(if (empty? entries)
[:div {:class (stl/css :history-entry-empty)}
[:div {:class (stl/css :history-entry-empty-icon)} deprecated-icon/history]
[:div {:class (stl/css :history-entry-empty-msg)} (tr "workspace.undo.empty")]]
[:> empty-state* {:icon i/history
:text (tr "workspace.undo.empty")}]]
[:ul {:class (stl/css :history-entries)}
(for [[idx-entry entry] (->> entries (map-indexed vector) reverse)] #_[i (range 0 10)]
[:& history-entry {:key (str "entry-" idx-entry)

View File

@@ -4,6 +4,7 @@
//
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "refactor/common-refactor.scss" as deprecated;
.history-toolbox {
@@ -30,23 +31,11 @@
}
.history-entry-empty {
@include deprecated.flexCenter;
display: flex;
flex-direction: column;
gap: deprecated.$s-16;
padding: deprecated.$s-28 deprecated.$s-16;
text-align: center;
}
.history-entry-empty-icon {
@extend .empty-icon;
svg {
margin-left: calc(-1 * deprecated.$s-2);
}
}
.history-entry-empty-msg {
@include deprecated.bodySmallTypography;
color: var(--empty-message-foreground-color);
gap: var(--sp-xxxl);
margin: var(--sp-xxxl) auto;
inline-size: $sz-200;
}
.history-entries {

View File

@@ -24,7 +24,8 @@
[app.main.ui.ds.controls.checkbox :refer [checkbox*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
@@ -775,29 +776,11 @@
(when (= (count interactions) 0)
[:div {:class (stl/css :section)}
[:div {:class (stl/css :content)}
[:div {:class (stl/css :help)}
[:div {:class (stl/css :empty)}
(when framed-shape?
[:div {:class (stl/css :help-group)}
[:div {:class (stl/css :help-icon)}
[:> icon* {:icon-id i/add
:size "l"
:class (stl/css :help-icon-inner)}]]
[:div {:class (stl/css :help-text)}
(tr "workspace.options.add-interaction")]])
[:div {:class (stl/css :help-group)}
[:div {:class (stl/css :help-icon)}
[:> icon* {:icon-id i/interaction
:size "l"
:class (stl/css :help-icon-inner)}]]
[:div {:class (stl/css :help-text)}
(tr "workspace.options.select-a-shape")]]
[:div {:class (stl/css :help-group)}
[:div {:class (stl/css :help-icon)}
[:> icon* {:icon-id i/play
:size "l"
:class (stl/css :help-icon-inner)}]]
[:div {:class (stl/css :help-text)}
(tr "workspace.options.use-play-button")]]]]])]))
[:> empty-state* {:icon i/add
:text (tr "workspace.options.add-interaction")}])
[:> empty-state* {:icon i/interaction
:text (tr "workspace.options.select-a-shape")}]
[:> empty-state* {:icon i/play
:text (tr "workspace.options.use-play-button")}]]]])]))

View File

@@ -43,40 +43,12 @@
gap: var(--sp-l);
}
.help {
.empty {
display: flex;
flex-direction: column;
gap: var(--sp-xxxl);
margin: var(--sp-xxxl) auto;
inline-size: $sz-200;
padding: var(--sp-xxxl) 0;
margin: 0 auto;
}
.help-group {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-m);
}
.help-text {
@include t.use-typography("body-small");
text-align: center;
color: var(--color-foreground-secondary);
}
.help-icon {
display: flex;
justify-content: center;
align-items: center;
inline-size: $sz-48;
block-size: $sz-48;
}
.help-icon-inner {
color: var(--color-foreground-secondary);
inline-size: $sz-32;
block-size: $sz-32;
}
.interaction-item {

View File

@@ -19,8 +19,9 @@
[app.main.ui.components.select :refer [select]]
[app.main.ui.dashboard.subscription :refer [get-subscription-type]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.cta :refer [cta*]]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.ds.product.milestone :refer [milestone*]]
[app.main.ui.ds.product.milestone-group :refer [milestone-group*]]
[app.util.dom :as dom]
@@ -385,8 +386,9 @@
(cond
(= status :loading)
[:div {:class (stl/css :versions-entry-empty)}
[:div {:class (stl/css :versions-entry-empty-msg)} (tr "workspace.versions.loading")]]
[:div {:class (stl/css :versions-empty)}
[:> empty-state* {:icon i/clock
:text (tr "workspace.versions.loading")}]]
(= status :loaded)
[:*
@@ -398,9 +400,9 @@
:icon i/pin}]]
(if (empty? data)
[:div {:class (stl/css :versions-entry-empty)}
[:div {:class (stl/css :versions-entry-empty-icon)} [:> icon* {:icon-id i/history}]]
[:div {:class (stl/css :versions-entry-empty-msg)} (tr "workspace.versions.empty")]]
[:div {:class (stl/css :versions-empty)}
[:> empty-state* {:icon i/history
:text (tr "workspace.versions.empty")}]]
[:ul {:class (stl/css :versions-entries)}
(for [entry entries]

View File

@@ -4,6 +4,7 @@
//
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as t;
@use "refactor/common-refactor.scss" as deprecated;
@@ -15,21 +16,12 @@
grid-template-rows: auto auto 1fr;
}
.versions-entry-empty {
align-items: center;
color: var(--color-foreground-secondary);
.versions-empty {
display: flex;
flex-direction: column;
font-size: deprecated.$fs-12;
gap: deprecated.$s-8;
padding: deprecated.$s-16;
}
.versions-entry-empty-icon {
background: var(--color-background-tertiary);
border-radius: 50%;
padding: deprecated.$s-8;
display: flex;
gap: var(--sp-xxxl);
margin: var(--sp-xxxl) auto;
inline-size: $sz-200;
}
.version-save-version {

View File

@@ -1844,8 +1844,8 @@ msgid "inspect.empty.help"
msgstr "If you want to know more about design inspect visit Penpot's help center"
#: src/app/main/ui/inspect/right_sidebar.cljs:240
msgid "inspect.empty.more-info"
msgstr "More info about inspect"
msgid "inspect.empty.more"
msgstr "More info"
#: src/app/main/ui/inspect/right_sidebar.cljs:232
msgid "inspect.empty.select"

View File

@@ -1835,8 +1835,8 @@ msgstr ""
"de Penpot"
#: src/app/main/ui/inspect/right_sidebar.cljs:240
msgid "inspect.empty.more-info"
msgstr "Más información sobre la inspección"
msgid "inspect.empty.more"
msgstr "Más información"
#: src/app/main/ui/inspect/right_sidebar.cljs:232
msgid "inspect.empty.select"