Add new components to storybook (#5632)

*  Add new components to storybook

*  Changes after review

*  More changes after review

*  Add file history components to the application

*  Unnest selector
This commit is contained in:
Alonso Torres 2025-01-28 12:34:15 +01:00 committed by GitHub
parent 0cf4d4636a
commit c215214120
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1033 additions and 210 deletions

View file

@ -1063,3 +1063,8 @@
(>= start 0) (< start size)
(>= end 0) (<= start end) (<= end size))
(subvec v start end)))))
(defn append-class
[class current-class]
(str (if (some? class) (str class " ") "")
current-class))

View file

@ -23,7 +23,12 @@ const preview = {
date: /Date$/i,
},
},
backgrounds: { disable: true },
backgrounds: {
values: [
{ name: 'theme', value: 'var(--color-background-secondary)' },
],
default: 'theme',
},
},
};

View file

@ -84,7 +84,8 @@
:controls :inline-actions
:type :inline
:level level
:actions [{:label "Refresh" :callback force-reload!}]
:accept {:label (tr "Refresh")
:callback force-reload!}
:tag :notification))
:maintenance
@ -92,8 +93,8 @@
:content (tr "notifications.by-code.maintenance")
:controls :inline-actions
:type level
:actions [{:label (tr "labels.accept")
:callback hide-notifications!}]
:accept {:label (tr "labels.accept")
:callback hide-notifications!}
:tag :notification))
(rx/of (ntf/dialog

View file

@ -32,6 +32,14 @@
[:or :string :keyword]]
[:timeout {:optional true}
[:maybe :int]]
[:accept {:optional true}
[:map
[:label :string]
[:callback ::sm/fn]]]
[:cancel {:optional true}
[:map
[:label :string]
[:callback ::sm/fn]]]
[:actions {:optional true}
[:vector
[:map
@ -120,7 +128,7 @@
:timeout timeout})))
(defn dialog
[& {:keys [content controls actions position tag level links]
[& {:keys [content controls actions accept cancel position tag level links]
:or {controls :none position :floating level :info}}]
(show (d/without-nils
{:content content
@ -129,4 +137,6 @@
:position position
:controls controls
:actions actions
:accept accept
:cancel cancel
:tag tag})))

View file

@ -1220,12 +1220,10 @@
:controls :inline-actions
:links [{:label (tr "workspace.updates.more-info")
:callback do-more-info}]
:actions [{:label (tr "workspace.updates.dismiss")
:type :secondary
:callback do-dismiss}
{:label (tr "workspace.updates.update")
:type :primary
:callback do-update}]
:cancel {:label (tr "workspace.updates.dismiss")
:callback do-dismiss}
:accept {:label (tr "workspace.updates.update")
:callback do-update}
:tag :sync-dialog)))))))

View file

@ -49,7 +49,7 @@
(ptk/reify ::fetch-versions
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)]
(when-let [file-id (:current-file-id state)]
(->> (rp/cmd! :get-file-snapshots {:file-id file-id})
(rx/map #(update-version-state {:status :loaded :data %})))))))

View file

@ -19,10 +19,16 @@
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon* token-status-list]]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.ds.notifications.actionable :refer [actionable*]]
[app.main.ui.ds.notifications.toast :refer [toast*]]
[app.main.ui.ds.product.autosaved-milestone :refer [autosaved-milestone*]]
[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.loader :refer [loader*]]
[app.main.ui.ds.product.user-milestone :refer [user-milestone*]]
[app.main.ui.ds.storybook :as sb]
[app.main.ui.ds.utilities.date :refer [date*]]
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
[app.util.i18n :as i18n]
[rumext.v2 :as mf]))
@ -46,8 +52,14 @@
:Text text*
:TabSwitcher tab-switcher*
:Toast toast*
:Actionable actionable*
:TokenStatusIcon token-status-icon*
:Swatch swatch*
:Cta cta*
:Avatar avatar*
:AutosavedMilestone autosaved-milestone*
:UserMilestone user-milestone*
:Date date*
;; meta / misc
:meta
{:icons (clj->js (sort icon-list))

View file

@ -11,6 +11,7 @@ $sz-16: px2rem(16);
$sz-24: px2rem(24);
$sz-32: px2rem(32);
$sz-36: px2rem(36);
$sz-40: px2rem(40);
$sz-48: px2rem(48);
$sz-160: px2rem(160);
$sz-200: px2rem(200);

View file

@ -74,8 +74,8 @@ $grayish-red: #bfbfbf;
--color-accent-secondary: #{$cobalt-700};
--color-accent-tertiary: #{$purple-600};
--color-accent-quaternary: #{$pink-400};
--color-accent-overlay: #{$purple-600-10};
--color-accent-select: #{$purple-700-60};
--color-accent-overlay: #{$purple-700-60};
--color-accent-select: #{$purple-600-10};
--color-accent-success: #{$green-500};
--color-background-success: #{$green-200};
@ -112,8 +112,8 @@ $grayish-red: #bfbfbf;
--color-accent-secondary: #{$purple-400};
--color-accent-tertiary: #{$mint-250};
--color-accent-quaternary: #{$pink-400};
--color-accent-overlay: #{$mint-250-10};
--color-accent-select: #{$mint-150-60};
--color-accent-overlay: #{$mint-150-60};
--color-accent-select: #{$mint-250-10};
--color-accent-success: #{$green-500};
--color-background-success: #{$green-950};

View file

@ -19,13 +19,14 @@
[:class {:optional true} :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
[:type {:optional true} :string]])
[:type {:optional true} :string]
[:variant {:optional true} :string]])
(mf/defc input*
{::mf/props :obj
::mf/forward-ref true
::mf/schema schema:input}
[{:keys [icon class type] :rest props} ref]
[{:keys [icon class type variant] :rest props} ref]
(let [ref (or ref (mf/use-ref))
type (d/nilv type "text")
props (mf/spread-props props
@ -43,7 +44,8 @@
(dom/select-node input-node)
(dom/focus! input-node))))]
[:> :span {:class (dm/str class " " (stl/css :container))}
[:> :span {:class (dm/str class " " (stl/css-case :container true
:input-seamless (= variant "seamless")))}
(when (some? icon)
[:> icon* {:icon-id icon :class (stl/css :icon) :on-click on-icon-click}])
[:> :input props]]))

View file

@ -41,12 +41,32 @@
}
}
.input-seamless {
--input-bg-color: none;
--input-outline-color: none;
--input-height: auto;
--input-margin: 0;
padding: 0;
border: none;
&:hover {
--input-bg-color: none;
--input-outline-color: none;
}
&:has(*:focus-visible) {
--input-bg-color: none;
--input-outline-color: none;
}
}
.input {
margin: unset; // remove settings from global css
margin: var(--input-margin, unset); // remove settings from global css
padding: 0;
appearance: none;
margin-inline-start: var(--sp-xxs);
height: $sz-32;
height: var(--input-height, #{$sz-32});
border: none;
background: none;
inline-size: 100%;

View file

@ -0,0 +1,50 @@
;; 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.notifications.actionable
(:require-macros
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.ui.ds.buttons.button :refer [button*]]
[rumext.v2 :as mf]))
(def ^:private schema:actionable
[:map
[:class {:optional true} :string]
[:variant {:optional true}
[:maybe [:enum "default" "error"]]]
[:acceptLabel {:optional true} :string]
[:cancelLabel {:optional true} :string]
[:onAccept {:optional true} [:fn fn?]]
[:onCancel {:optional true} [:fn fn?]]])
(mf/defc actionable*
{::mf/props :obj
::mf/schema schema:actionable}
[{:keys [class variant acceptLabel cancelLabel children onAccept onCancel] :rest props}]
(let [variant (or variant "default")
class (d/append-class class (stl/css :notification))
props (mf/spread-props props {:class class :data-testid "actionable"})
handle-accept
(mf/use-fn
(fn [e]
(when onAccept (onAccept e))))
handle-cancel
(mf/use-fn
(fn [e]
(when onCancel (onCancel e))))]
[:> "aside" props
[:div {:class (stl/css :notification-message)}
children]
[:> button* {:variant "secondary"
:on-click handle-cancel} cancelLabel]
[:> button* {:variant (if (= variant "default") "primary" "destructive")
:on-click handle-accept} acceptLabel]]))

View file

@ -0,0 +1,25 @@
// 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 "../typography.scss" as t;
.notification {
align-items: center;
background: var(--color-background-primary);
border-radius: $br-8;
border: $b-1 solid var(--color-background-quaternary);
display: grid;
gap: var(--sp-s);
grid-template-columns: 1fr auto auto;
justify-content: space-between;
padding: var(--sp-s);
}
.notification-message {
@include t.use-typography("body-small");
color: var(--color-foreground-primary);
}

View file

@ -0,0 +1,39 @@
import * as React from "react";
import Components from "@target/components";
const { Actionable } = Components;
export default {
title: "Notifications/Actionable",
component: Actionable,
argTypes: {
variant: {
options: ["default", "error"],
control: { type: "select" },
},
acceptLabel: {
control: { type: "text" },
},
cancelLabel: {
control: { type: "text" },
},
},
args: {
variant: "default",
acceptLabel: "Update",
cancelLabel: "Dismiss",
},
render: ({ ...args }) => (
<Actionable {...args}>
Message for the notification <a href="#">more info</a>
</Actionable>
),
};
export const Default = {};
export const Error = {
args: {
variant: "error",
},
};

View file

@ -0,0 +1,70 @@
;; 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.autosaved-milestone
(:require-macros
[app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.utilities.date :refer [date* valid-date?]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(def ^:private schema:milestone
[:map
[:class {:optional true} :string]
[:active {:optional true} :boolean]
[:versionToggled {:optional true} :boolean]
[:label :string]
[:autosavedMessage :string]
[:snapshots [:vector [:fn valid-date?]]]])
(mf/defc autosaved-milestone*
{::mf/props :obj
::mf/schema schema:milestone}
[{:keys [class active versionToggled label autosavedMessage snapshots
onClickSnapshotMenu onToggleExpandSnapshots] :rest props}]
(let [class (d/append-class class (stl/css-case :milestone true :is-selected active))
props (mf/spread-props props {:class class :data-testid "milestone"})
handle-click-menu
(mf/use-fn
(mf/deps onClickSnapshotMenu)
(fn [event]
(let [index (-> (dom/get-current-target event)
(dom/get-data "index")
(d/parse-integer))]
(when onClickSnapshotMenu
(onClickSnapshotMenu event index)))))]
[:> "div" props
[:> text* {:as "span" :typography t/body-small :class (stl/css :name)} label]
[:div {:class (stl/css :snapshots)}
[:button {:class (stl/css :toggle-snapshots)
:aria-label (tr "workspace.versions.expand-snapshot")
:on-click onToggleExpandSnapshots}
[:> i/icon* {:icon-id i/clock :class (stl/css :icon-clock)}]
[:> text* {:as "span" :typography t/body-medium :class (stl/css :toggle-message)} autosavedMessage]
[:> i/icon* {:icon-id i/arrow :class (stl/css-case :icon-arrow true :icon-arrow-toggled versionToggled)}]]
(when versionToggled
(for [[idx d] (d/enumerate snapshots)]
[:div {:key (dm/str "entry-" idx)
:class (stl/css :version-entry)}
[:> date* {:date d :class (stl/css :date) :typography t/body-small}]
[:> icon-button* {:class (stl/css :entry-button)
:variant "ghost"
:icon "menu"
:aria-label (tr "workspace.versions.version-menu")
:data-index idx
:on-click handle-click-menu}]]))]]))

View file

@ -0,0 +1,117 @@
// 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 "../_sizes.scss" as *;
@use "../_borders.scss" as *;
@use "../typography.scss" as t;
.milestone {
border: $b-1 solid var(--border-color, transparent);
border-radius: $br-8;
cursor: pointer;
background: var(--color-background-primary);
display: grid;
grid-template-areas:
"avatar name button"
"avatar content button";
grid-template-rows: auto 1fr;
grid-template-columns: calc(var(--sp-xxl) + var(--sp-l)) 1fr auto;
padding: var(--sp-s) 0;
align-items: center;
column-gap: var(--sp-s);
&.is-selected,
&:hover {
--border-color: var(--color-accent-primary);
}
}
.avatar {
grid-area: avatar;
justify-self: flex-end;
}
.name {
@include t.use-typography("body-small");
grid-area: name;
color: var(--color-foreground-primary);
}
.toggle-message {
@include t.use-typography("body-small");
grid-area: name;
}
.date {
grid-area: content;
color: var(--date-color, var(--color-foreground-secondary));
&:hover {
--date-color: var(--color-accent-primary);
}
}
.snapshots {
grid-area: content;
}
.milestone-buttons {
grid-area: button;
display: flex;
padding-right: var(--sp-xs);
}
.version-entry {
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas: "content button";
align-items: center;
&:hover {
--date-color: var(--color-accent-primary);
--display-button: visible;
}
}
.toggle-snapshots {
background: none;
border: none;
color: var(--color-foreground-secondary);
display: flex;
flex-direction: row;
gap: var(--sp-xs);
align-items: flex-end;
margin: 0;
margin-top: var(--sp-xxs);
margin-bottom: var(--sp-s);
padding: 0;
cursor: pointer;
&:hover {
color: var(--color-accent-primary);
--icon-stroke-color: var(--color-accent-primary);
}
}
.icon-clock {
--icon-stroke-color: var(--color-accent-warning);
}
.icon-arrow {
--icon-stroke-color: var(--icon-stroke-color, var(--color-foreground-secondary));
}
.icon-arrow-toggled {
transform: rotate(90deg);
}
.entry-button {
visibility: var(--display-button, hidden);
}

View file

@ -0,0 +1,44 @@
import * as React from "react";
import Components from "@target/components";
const { AutosavedMilestone } = Components;
export default {
title: "Product/Milestones/Autosaved",
component: AutosavedMilestone,
argTypes: {
label: {
control: { type: "text" },
},
active: {
control: { type: "boolean" },
},
autosaved: {
control: { type: "boolean" },
},
versionToggled: {
control: { type: "boolean" },
},
snapshots: {
control: { type: "object" },
},
},
args: {
label: "Milestone 1",
active: false,
versionToggled: false,
snapshots: [1737452413841, 1737452422063, 1737452431603],
autosavedMessage: "3 autosave versions",
},
render: ({ ...args }) => {
const user = {
name: args.userName,
avatar: args.userAvatar,
color: args.userColor,
};
return <AutosavedMilestone user={user} {...args} />;
},
};
export const Default = {};

View file

@ -0,0 +1,46 @@
;; 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.avatar
(:require-macros
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.util.avatars :as avatars]
[rumext.v2 :as mf]))
(def ^:private schema:avatar
[:map
[:class {:optional true} :string]
[:tag {:optional true} :string]
[:name {:optional true} [:maybe :string]]
[:url {:optional true} [:maybe :string]]
[:color {:optional true} [:maybe :string]]
[:selected {:optional true} :boolean]
[:variant {:optional true}
[:maybe [:enum "S" "M" "L"]]]])
(mf/defc avatar*
{::mf/props :obj
::mf/schema schema:avatar}
[{:keys [tag class name color url selected variant] :rest props}]
(let [variant (or variant "S")
url (if (and (some? url) (d/not-empty? url))
url
(avatars/generate {:name name :color color}))]
[:> (or tag "div")
{:class (d/append-class
class
(stl/css-case :avatar true
:avatar-small (= variant "S")
:avatar-medium (= variant "M")
:avatar-large (= variant "L")
:is-selected selected))
:style {"--avatar-color" color}
:title name}
[:div {:class (stl/css :avatar-image)}
[:img {:alt name :src url}]]]))

View file

@ -0,0 +1,47 @@
// 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 "../_sizes.scss" as *;
@use "../_borders.scss" as *;
.avatar {
border-radius: $br-circle;
overflow: hidden;
border: $b-1 solid var(--border-color, none);
}
.avatar-small {
width: $sz-24;
height: $sz-24;
}
.avatar-medium {
width: $sz-32;
height: $sz-32;
}
.avatar-large {
width: $sz-40;
height: $sz-40;
}
.avatar-image {
overflow: hidden;
border-radius: $br-circle;
background-color: var(--avatar-color);
}
.is-selected {
--border-color: var(--color-accent-primary);
padding: var(--sp-xxs);
}
.avatar {
&:hover,
&:focus {
--border-color: var(--color-accent-primary);
}
}

View file

@ -0,0 +1,67 @@
import * as React from "react";
import Components from "@target/components";
const { Avatar } = Components;
export default {
title: "Product/Avatar",
component: Avatar,
argTypes: {
name: {
control: { type: "text" },
},
url: {
control: { type: "text" },
},
color: {
control: { type: "color" },
},
variant: {
options: ["S", "M", "L"],
control: { type: "select" },
},
selected: {
control: { type: "boolean" },
},
},
args: {
name: "Ada Lovelace",
url: "/images/avatar-blue.jpg",
color: "#79d4ff",
variant: "S",
selected: false,
},
render: ({ ...args }) => <Avatar profile={{ fullname: "TEST" }} {...args} />,
};
export const Default = {};
export const NoURL = {
args: {
url: null,
},
};
export const Small = {
args: {
variant: "S",
},
};
export const Medium = {
args: {
variant: "M",
},
};
export const Large = {
args: {
variant: "L",
},
};
export const Selected = {
args: {
selected: true,
},
};

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
(ns app.main.ui.ds.product.cta
(:require-macros
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[rumext.v2 :as mf]))
(def ^:private schema:cta
[:map
[:class {:optional true} :string]
[:title :string]])
(mf/defc cta*
{::mf/props :obj
::mf/schema schema:cta}
[{:keys [class title children] :rest props}]
(let [class (d/append-class class (stl/css :cta))
props (mf/spread-props props {:class class :data-testid "cta"})]
[:> "div" props
[:div {:class (stl/css :cta-title)}
[:> text* {:as "span" :typography t/headline-small :class (stl/css :placeholder-title)} title]]
[:div {:class (stl/css :cta-message)}
children]]))

View file

@ -0,0 +1,21 @@
// 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 "../colors.scss" as *;
@use "../_sizes.scss" as *;
@use "../_borders.scss" as *;
@use "../typography.scss" as t;
.cta {
border-radius: $br-8;
border: $b-1 solid var(--color-accent-primary-muted);
background: var(--color-accent-select);
padding: var(--sp-m);
}
.cta-title {
color: var(--color-foreground-primary);
}

View file

@ -0,0 +1,34 @@
import * as React from "react";
import Components from "@target/components";
const { Cta } = Components;
export default {
title: "Product/CTA",
component: Cta,
argTypes: {
title: {
control: { type: "text" },
},
},
args: {
title: "Autosaved versions will be kept for 7 days.",
},
render: ({ ...args }) => (
<Cta {...args}>
<span
style={{
fontSize: "0.75rem",
color: "var(--color-foreground-secondary)",
}}
>
If youd like to increase this limit, write to us at{" "}
<a style={{ color: "var(--color-accent-primary)" }} href="#">
support@penpot.app
</a>
</span>
</Cta>
),
};
export const Default = {};

View file

@ -0,0 +1,77 @@
;; 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.user-milestone
(:require-macros
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.product.avatar :refer [avatar*]]
[app.main.ui.ds.utilities.date :refer [valid-date?]]
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
[app.util.time :as dt]
[rumext.v2 :as mf]))
(def ^:private schema:milestone
[:map
[:class {:optional true} :string]
[:active {:optional true} :boolean]
[:editing {:optional true} :boolean]
[:user
[:map
[:name {:optional true} [:maybe :string]]
[:avatar {:optional true} [:maybe :string]]
[:color {:optional true} [:maybe :string]]]]
[:label :string]
[:date [:fn valid-date?]]
[:onOpenMenu {:optional true} [:maybe [:fn fn?]]]
[:onFocusInput {:optional true} [:maybe [:fn fn?]]]
[:onBlurInput {:optional true} [:maybe [:fn fn?]]]
[:onKeyDownInput {:optional true} [:maybe [:fn fn?]]]])
(mf/defc user-milestone*
{::mf/props :obj
::mf/schema schema:milestone}
[{:keys [class active editing user label date
onOpenMenu onFocusInput onBlurInput onKeyDownInput] :rest props}]
(let [class (d/append-class class (stl/css-case :milestone true :is-selected active))
props (mf/spread-props props {:class class :data-testid "milestone"})
date (cond-> date (not (dt/datetime? date)) dt/datetime)
time (dt/timeago date)]
[:> "div" props
[:> avatar* {:name (obj/get user "name")
:url (obj/get user "avatar")
:color (obj/get user "color")
:variant "S" :class (stl/css :avatar)}]
(if editing
[:> input*
{:class (stl/css :name-input)
:variant "seamless"
:default-value label
:auto-focus true
:on-focus onFocusInput
:on-blur onBlurInput
:on-key-down onKeyDownInput}]
[:> text* {:as "span" :typography t/body-small :class (stl/css :name)} label])
[:*
[:time {:dateTime (dt/format date :iso)
:class (stl/css :date)} time]
[:div {:class (stl/css :milestone-buttons)}
[:> icon-button* {:class (stl/css :menu-button)
:variant "ghost"
:icon "menu"
:aria-label (tr "workspace.versions.version-menu")
:on-click onOpenMenu}]]]]))

View file

@ -0,0 +1,64 @@
// 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 "../_sizes.scss" as *;
@use "../_borders.scss" as *;
@use "../typography.scss" as t;
.milestone {
border: $b-1 solid var(--border-color, transparent);
border-radius: $br-8;
cursor: pointer;
background: var(--color-background-primary);
display: grid;
grid-template-areas:
"avatar name button"
"avatar content button";
grid-template-rows: auto 1fr;
grid-template-columns: calc(var(--sp-xxl) + var(--sp-l)) 1fr auto;
padding: var(--sp-s) 0;
align-items: center;
column-gap: var(--sp-s);
&.is-selected,
&:hover {
--border-color: var(--color-accent-primary);
}
}
.name-input {
grid-area: name;
}
.avatar {
grid-area: avatar;
justify-self: flex-end;
}
.name {
grid-area: name;
color: var(--color-foreground-primary);
}
.date {
@include t.use-typography("body-small");
grid-area: content;
color: var(--color-foreground-secondary);
}
.snapshots {
grid-area: content;
}
.milestone-buttons {
grid-area: button;
display: flex;
padding-right: var(--sp-xs);
}

View file

@ -0,0 +1,61 @@
import * as React from "react";
import Components from "@target/components";
const { UserMilestone } = Components;
export default {
title: "Product/Milestones/User",
component: UserMilestone,
argTypes: {
userName: {
control: { type: "text" },
},
userAvatar: {
control: { type: "text" },
},
userColor: {
control: { type: "color" },
},
label: {
control: { type: "text" },
},
date: {
control: { type: "date" },
},
active: {
control: { type: "boolean" },
},
editing: {
control: { type: "boolean" },
},
autosaved: {
control: { type: "boolean" },
},
versionToggled: {
control: { type: "boolean" },
},
snapshots: {
control: { type: "object" },
},
},
args: {
label: "Milestone 1",
userName: "Ada Lovelace",
userAvatar: "/images/avatar-blue.jpg",
userColor: "#79d4ff",
date: 1735686000000,
active: false,
editing: false,
},
render: ({ ...args }) => {
const user = {
name: args.userName,
avatar: args.userAvatar,
color: args.userColor,
};
return <UserMilestone user={user} {...args} />;
},
};
export const Default = {};

View file

@ -0,0 +1,42 @@
;; 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.date
(:require-macros
[app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.util.time :as dt]
[rumext.v2 :as mf]))
(defn valid-date?
[date]
(or (dt/datetime? date) (number? date)))
(def ^:private schema:date
[:map
[:class {:optional true} :string]
[:as {:optional true} :string]
[:date [:fn valid-date?]]
[:selected {:optional true} :boolean]
[:typography {:optional true} :string]])
(mf/defc date*
{::mf/props :obj
::mf/schema schema:date}
[{:keys [class date selected typography] :rest props}]
(let [class (d/append-class class (stl/css-case :date true :is-selected selected))
date (cond-> date (not (dt/datetime? date)) dt/datetime)
typography (or typography t/body-medium)]
[:> text* {:as "time" :typography typography :class class :dateTime (dt/format date :iso)}
(dm/str
(dt/format date :date-full)
" . "
(dt/format date :time-24-simple)
"h")]))

View file

@ -0,0 +1,13 @@
// 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
.date {
color: var(--date-color, var(--color-foreground-secondary));
}
.is-selected {
color: var(--date-color, var(--color-accent-primary));
}

View file

@ -0,0 +1,25 @@
import * as React from "react";
import Components from "@target/components";
const { Date } = Components;
export default {
title: "Foundations/Utilities/Date",
component: Date,
argTypes: {
date: {
control: { type: "date" },
},
selected: {
control: { type: "boolean" },
},
},
args: {
title: "Date",
date: 1735686000000,
selected: false,
},
render: ({ ...args }) => <Date {...args} />,
};
export const Default = {};

View file

@ -39,7 +39,8 @@
inline?
[:& inline-notification
{:actions (:actions notification)
{:accept (:accept notification)
:cancel (:cancel notification)
:links (:links notification)
:content (:content notification)}]

View file

@ -9,37 +9,28 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.uuid :as uuid]
[app.main.ui.components.link-button :as lb]
[app.main.ui.ds.notifications.actionable :refer [actionable*]]
[rumext.v2 :as mf]))
(mf/defc inline-notification
"They are persistent messages and report a special situation
of the application and require user interaction to disappear."
{::mf/props :obj}
[{:keys [content actions links] :as props}]
[:aside {:class (stl/css :inline-notification)}
[:div {:class (stl/css :inline-text)}
[{:keys [content accept cancel links] :as props}]
content
[:> actionable* {:class (stl/css :new-inline)
:cancelLabel (:label cancel)
:onCancel (:callback cancel)
:acceptLabel (:label accept)
:onAccept (:callback accept)}
content
(when (some? links)
[:nav {:class (stl/css :link-nav)}
(for [[index link] (d/enumerate links)]
[:& lb/link-button {:key (dm/str "link-" index)
:class (stl/css :link)
:on-click (:callback link)
:value (:label link)}])])]
[:div {:class (stl/css :actions)}
(for [action actions]
[:button {:key (uuid/next)
:class (stl/css-case :action-btn true
:primary (= :primary (:type action))
:secondary (= :secondary (:type action))
:danger (= :danger (:type action)))
:on-click (:callback action)}
(:label action)])]])
(when (some? links)
[:nav {:class (stl/css :link-nav)}
(for [[index link] (d/enumerate links)]
[:& lb/link-button {:key (dm/str "link-" index)
:class (stl/css :link)
:on-click (:callback link)
:value (:label link)}])])])

View file

@ -6,73 +6,15 @@
@import "refactor/common-refactor.scss";
.inline-notification {
--inline-notification-bg-color: var(--alert-background-color-default);
--inline-notification-fg-color: var(--alert-text-foreground-color-default);
--inline-notification-border-color: var(--alert-border-color-default);
@include alertShadow;
.new-inline {
position: absolute;
top: $s-72;
margin: auto;
left: 0;
right: 0;
display: grid;
grid-template-columns: 1fr auto;
gap: $s-24;
min-height: $s-48;
min-width: $s-640;
width: fit-content;
max-width: $s-960;
padding: $s-8;
margin-inline: auto;
border: $s-1 solid var(--inline-notification-border-color);
border-radius: $br-8;
z-index: $z-index-modal;
background-color: var(--inline-notification-bg-color);
color: var(--inline-notification-fg-color);
}
.inline-text {
@include bodySmallTypography;
align-self: center;
}
.link-nav {
display: inline;
}
.link {
@include bodySmallTypography;
margin: 0;
height: 100%;
color: var(--modal-link-foreground-color);
}
.actions {
display: grid;
grid-template-columns: none;
grid-auto-flow: column;
align-self: center;
gap: $s-8;
}
.action-btn {
@extend .button-secondary;
@include uppercaseTitleTipography;
min-height: $s-32;
min-width: $s-32;
width: fit-content;
padding: $s-8 $s-24;
border: $s-1 solid transparent;
}
.action-btn.primary {
@extend .button-primary;
}
.action-btn.secondary {
@extend .button-secondary;
}
.action-btn.danger {
@extend .modal-danger-btn;
}

View file

@ -7,7 +7,6 @@
(ns app.main.ui.workspace.sidebar.versions
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
@ -20,6 +19,9 @@
[app.main.ui.components.select :refer [select]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.autosaved-milestone :refer [autosaved-milestone*]]
[app.main.ui.ds.product.cta :refer [cta*]]
[app.main.ui.ds.product.user-milestone :refer [user-milestone*]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
@ -51,9 +53,7 @@
(mf/defc version-entry
[{:keys [entry profile on-restore-version on-delete-version on-rename-version editing?]}]
(let [input-ref (mf/use-ref nil)
show-menu? (mf/use-state false)
(let [show-menu? (mf/use-state false)
handle-open-menu
(mf/use-fn
@ -112,35 +112,16 @@
(st/emit! (dwv/update-version-state {:editing nil})))))]
[:li {:class (stl/css :version-entry-wrap)}
[:div {:class (stl/css :version-entry :is-snapshot)}
[:img {:class (stl/css :version-entry-avatar)
:alt (:fullname profile)
:src (cfg/resolve-profile-photo-url profile)}]
[:div {:class (stl/css :version-entry-data)}
(if editing?
[:input {:class (stl/css :version-entry-name-edit)
:type "text"
:ref input-ref
:on-focus handle-name-input-focus
:on-blur handle-name-input-blur
:on-key-down handle-name-input-key-down
:auto-focus true
:default-value (:label entry)}]
[:p {:class (stl/css :version-entry-name)}
(:label entry)])
[:p {:class (stl/css :version-entry-time)}
(let [locale (mf/deref i18n/locale)
time (dt/timeago (:created-at entry) {:locale locale})]
[:span {:class (stl/css :date)} time])]]
[:> icon-button* {:class (stl/css :version-entry-options)
:variant "ghost"
:aria-label (tr "workspace.versions.version-menu")
:on-click handle-open-menu
:icon "menu"}]]
[:> user-milestone* {:label (:label entry)
:user #js {:name (:fullname profile)
:avatar (cfg/resolve-profile-photo-url profile)
:color (:color profile)}
:editing editing?
:date (:created-at entry)
:onOpenMenu handle-open-menu
:onFocusInput handle-name-input-focus
:onBlurInput handle-name-input-blur
:onKeyDownInput handle-name-input-key-down}]
[:& dropdown {:show @show-menu? :on-close handle-close-menu}
[:ul {:class (stl/css :version-options-dropdown)}
@ -158,6 +139,7 @@
[{:keys [index is-expanded entry on-toggle-expand on-pin-snapshot on-restore-snapshot]}]
(let [open-menu (mf/use-state nil)
entry-ref (mf/use-ref nil)
handle-toggle-expand
(mf/use-fn
@ -180,51 +162,45 @@
(fn [event]
(let [node (dom/get-current-target event)
id (-> (dom/get-data node "id") uuid/uuid)]
(when on-restore-snapshot (on-restore-snapshot id)))))]
(when on-restore-snapshot (on-restore-snapshot id)))))
[:li {:class (stl/css :version-entry-wrap)}
[:div {:class (stl/css-case :version-entry true
:is-autosave true
:is-expanded is-expanded)}
[:p {:class (stl/css :version-entry-name)}
(tr "workspace.versions.autosaved.version" (dt/format (:created-at entry) :date-full))]
[:button {:class (stl/css :version-entry-snapshots)
:aria-label (tr "workspace.versions.expand-snapshot")
:on-click handle-toggle-expand}
[:> i/icon* {:icon-id i/clock :class (stl/css :icon-clock)}]
(tr "workspace.versions.autosaved.entry" (count (:snapshots entry)))
[:> i/icon* {:icon-id i/arrow :class (stl/css :icon-arrow)}]]
handle-open-snapshot-menu
(mf/use-fn
(mf/deps entry)
(fn [event index]
(let [snapshot (nth (:snapshots entry) index)
current-bb (-> entry-ref mf/ref-val dom/get-bounding-rect :top)
target-bb (-> event dom/get-target dom/get-bounding-rect :top)
offset (+ (- target-bb current-bb) 32)]
(swap! open-menu assoc
:snapshot (:id snapshot)
:offset offset))))]
[:ul {:class (stl/css :version-snapshot-list)}
(for [[idx snapshot] (d/enumerate (:snapshots entry))]
[:li {:class (stl/css :version-snapshot-entry-wrapper)
:key (dm/str "snp-" idx)}
[:div {:class (stl/css :version-snapshot-entry)}
(str
(dt/format (:created-at snapshot) :date-full)
" . "
(dt/format (:created-at snapshot) :time-24-simple))]
[:li {:ref entry-ref :class (stl/css :version-entry-wrap)}
[:> autosaved-milestone*
{:label (tr "workspace.versions.autosaved.version"
(dt/format (:created-at entry) :date-full))
:autosavedMessage (tr "workspace.versions.autosaved.entry" (count (:snapshots entry)))
:snapshots (mapv :created-at (:snapshots entry))
:versionToggled is-expanded
:onClickSnapshotMenu handle-open-snapshot-menu
:onToggleExpandSnapshots handle-toggle-expand}]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.versions.snapshot-menu")
:on-click #(reset! open-menu snapshot)
:icon "menu"
:class (stl/css :version-snapshot-menu-btn)}]
[:& dropdown {:show (= @open-menu snapshot)
:on-close #(reset! open-menu nil)}
[:ul {:class (stl/css :version-options-dropdown)}
[:li {:class (stl/css :menu-option)
:role "button"
:data-id (dm/str (:id snapshot))
:on-click handle-restore-snapshot}
(tr "workspace.versions.button.restore")]
[:li {:class (stl/css :menu-option)
:role "button"
:data-id (dm/str (:id snapshot))
:on-click handle-pin-snapshot}
(tr "workspace.versions.button.pin")]]]])]]]))
[:& dropdown {:show (some? @open-menu)
:on-close #(reset! open-menu nil)}
[:ul {:class (stl/css :version-options-dropdown)
:style {"--offset" (dm/str (:offset @open-menu) "px")}}
[:li {:class (stl/css :menu-option)
:role "button"
:data-id (dm/str (:snapshot @open-menu))
:on-click handle-restore-snapshot}
(tr "workspace.versions.button.restore")]
[:li {:class (stl/css :menu-option)
:role "button"
:data-id (dm/str (:snapshot @open-menu))
:on-click handle-pin-snapshot}
(tr "workspace.versions.button.pin")]]]]))
(mf/defc versions-toolbox*
[]
@ -282,12 +258,10 @@
(ntf/dialog
:content (tr "workspace.versions.restore-warning")
:controls :inline-actions
:actions [{:label (tr "workspace.updates.dismiss")
:type :secondary
:callback #(st/emit! (ntf/hide))}
{:label (tr "labels.restore")
:type :primary
:callback #(st/emit! (dwv/restore-version id origin))}]
:cancel {:label (tr "workspace.updates.dismiss")
:callback #(st/emit! (ntf/hide))}
:accept {:label (tr "labels.restore")
:callback #(st/emit! (dwv/restore-version id origin))}
:tag :restore-dialog))))
handle-restore-version-pinned
@ -384,12 +358,9 @@
nil))])
[:div {:class (stl/css :autosave-warning)}
[:div {:class (stl/css :autosave-warning-text)}
(tr "workspace.versions.warning.text" versions-stored-days)]
[:div {:class (stl/css :autosave-warning-subtext)}
[:> i18n/tr-html*
{:tag-name "div"
:content (tr "workspace.versions.warning.subtext"
"mailto:support@penpot.app")}]]]])]))
[:> cta* {:title (tr "workspace.versions.warning.text" versions-stored-days)}
[:> i18n/tr-html*
{:tag-name "div"
:class (stl/css :cta)
:content (tr "workspace.versions.warning.subtext"
"mailto:support@penpot.app")}]]])]))

View file

@ -4,6 +4,7 @@
//
// Copyright (c) KALEIDOS INC
@use "../../ds/typography.scss" as t;
@import "refactor/common-refactor.scss";
.version-toolbox {
@ -145,6 +146,7 @@
max-width: $s-200;
right: 0;
left: unset;
top: var(--offset);
.menu-option {
@extend .dropdown-element-base;
}
@ -231,22 +233,10 @@
visibility: hidden;
}
.autosave-warning {
display: flex;
flex-direction: column;
gap: $s-8;
padding: $s-16;
}
.autosave-warning-text {
color: var(--color-foreground-primary);
font-size: $fs-12;
text-transform: uppercase;
}
.autosave-warning-subtext {
.cta {
@include t.use-typography("body-small");
color: var(--color-foreground-secondary);
font-size: $fs-12;
a {
color: var(--color-accent-primary);
}