mirror of
https://github.com/penpot/penpot.git
synced 2025-08-03 19:48:22 +02:00
✨ 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:
parent
0cf4d4636a
commit
c215214120
34 changed files with 1033 additions and 210 deletions
|
@ -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))
|
||||
|
|
|
@ -23,7 +23,12 @@ const preview = {
|
|||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
backgrounds: { disable: true },
|
||||
backgrounds: {
|
||||
values: [
|
||||
{ name: 'theme', value: 'var(--color-background-secondary)' },
|
||||
],
|
||||
default: 'theme',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})))
|
||||
|
|
|
@ -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)))))))
|
||||
|
||||
|
||||
|
|
|
@ -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 %})))))))
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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]]))
|
||||
|
|
|
@ -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%;
|
||||
|
|
50
frontend/src/app/main/ui/ds/notifications/actionable.cljs
Normal file
50
frontend/src/app/main/ui/ds/notifications/actionable.cljs
Normal 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]]))
|
25
frontend/src/app/main/ui/ds/notifications/actionable.scss
Normal file
25
frontend/src/app/main/ui/ds/notifications/actionable.scss
Normal 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);
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
};
|
70
frontend/src/app/main/ui/ds/product/autosaved_milestone.cljs
Normal file
70
frontend/src/app/main/ui/ds/product/autosaved_milestone.cljs
Normal 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}]]))]]))
|
||||
|
117
frontend/src/app/main/ui/ds/product/autosaved_milestone.scss
Normal file
117
frontend/src/app/main/ui/ds/product/autosaved_milestone.scss
Normal 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);
|
||||
}
|
|
@ -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 = {};
|
46
frontend/src/app/main/ui/ds/product/avatar.cljs
Normal file
46
frontend/src/app/main/ui/ds/product/avatar.cljs
Normal 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}]]]))
|
47
frontend/src/app/main/ui/ds/product/avatar.scss
Normal file
47
frontend/src/app/main/ui/ds/product/avatar.scss
Normal 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);
|
||||
}
|
||||
}
|
67
frontend/src/app/main/ui/ds/product/avatar.stories.jsx
Normal file
67
frontend/src/app/main/ui/ds/product/avatar.stories.jsx
Normal 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,
|
||||
},
|
||||
};
|
32
frontend/src/app/main/ui/ds/product/cta.cljs
Normal file
32
frontend/src/app/main/ui/ds/product/cta.cljs
Normal 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]]))
|
21
frontend/src/app/main/ui/ds/product/cta.scss
Normal file
21
frontend/src/app/main/ui/ds/product/cta.scss
Normal 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);
|
||||
}
|
34
frontend/src/app/main/ui/ds/product/cta.stories.jsx
Normal file
34
frontend/src/app/main/ui/ds/product/cta.stories.jsx
Normal 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 you’d 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 = {};
|
77
frontend/src/app/main/ui/ds/product/user_milestone.cljs
Normal file
77
frontend/src/app/main/ui/ds/product/user_milestone.cljs
Normal 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}]]]]))
|
||||
|
||||
|
64
frontend/src/app/main/ui/ds/product/user_milestone.scss
Normal file
64
frontend/src/app/main/ui/ds/product/user_milestone.scss
Normal 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);
|
||||
}
|
|
@ -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 = {};
|
42
frontend/src/app/main/ui/ds/utilities/date.cljs
Normal file
42
frontend/src/app/main/ui/ds/utilities/date.cljs
Normal 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")]))
|
13
frontend/src/app/main/ui/ds/utilities/date.scss
Normal file
13
frontend/src/app/main/ui/ds/utilities/date.scss
Normal 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));
|
||||
}
|
25
frontend/src/app/main/ui/ds/utilities/date.stories.jsx
Normal file
25
frontend/src/app/main/ui/ds/utilities/date.stories.jsx
Normal 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 = {};
|
|
@ -39,7 +39,8 @@
|
|||
|
||||
inline?
|
||||
[:& inline-notification
|
||||
{:actions (:actions notification)
|
||||
{:accept (:accept notification)
|
||||
:cancel (:cancel notification)
|
||||
:links (:links notification)
|
||||
:content (:content notification)}]
|
||||
|
||||
|
|
|
@ -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)}])])])
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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")}]]])]))
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue