diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc
index 649aff34db..97ddc75bb7 100644
--- a/common/src/app/common/data.cljc
+++ b/common/src/app/common/data.cljc
@@ -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))
diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js
index 2f94839394..943e0133dc 100644
--- a/frontend/.storybook/preview.js
+++ b/frontend/.storybook/preview.js
@@ -23,7 +23,12 @@ const preview = {
date: /Date$/i,
},
},
- backgrounds: { disable: true },
+ backgrounds: {
+ values: [
+ { name: 'theme', value: 'var(--color-background-secondary)' },
+ ],
+ default: 'theme',
+ },
},
};
diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs
index 7b833e8969..cd8e731862 100644
--- a/frontend/src/app/main/data/common.cljs
+++ b/frontend/src/app/main/data/common.cljs
@@ -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
diff --git a/frontend/src/app/main/data/notifications.cljs b/frontend/src/app/main/data/notifications.cljs
index d65ea5486b..7babc171c6 100644
--- a/frontend/src/app/main/data/notifications.cljs
+++ b/frontend/src/app/main/data/notifications.cljs
@@ -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})))
diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs
index 8af6545be8..fa8157e6cb 100644
--- a/frontend/src/app/main/data/workspace/libraries.cljs
+++ b/frontend/src/app/main/data/workspace/libraries.cljs
@@ -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)))))))
diff --git a/frontend/src/app/main/data/workspace/versions.cljs b/frontend/src/app/main/data/workspace/versions.cljs
index 250d13deb6..ee710883aa 100644
--- a/frontend/src/app/main/data/workspace/versions.cljs
+++ b/frontend/src/app/main/data/workspace/versions.cljs
@@ -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 %})))))))
diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs
index 395e08c07b..063f2bf874 100644
--- a/frontend/src/app/main/ui/ds.cljs
+++ b/frontend/src/app/main/ui/ds.cljs
@@ -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))
diff --git a/frontend/src/app/main/ui/ds/_sizes.scss b/frontend/src/app/main/ui/ds/_sizes.scss
index aea2066088..b6bd14c467 100644
--- a/frontend/src/app/main/ui/ds/_sizes.scss
+++ b/frontend/src/app/main/ui/ds/_sizes.scss
@@ -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);
diff --git a/frontend/src/app/main/ui/ds/colors.scss b/frontend/src/app/main/ui/ds/colors.scss
index 773ee1afcc..32d2403dfc 100644
--- a/frontend/src/app/main/ui/ds/colors.scss
+++ b/frontend/src/app/main/ui/ds/colors.scss
@@ -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};
diff --git a/frontend/src/app/main/ui/ds/controls/input.cljs b/frontend/src/app/main/ui/ds/controls/input.cljs
index 279e06f9af..266f15ef33 100644
--- a/frontend/src/app/main/ui/ds/controls/input.cljs
+++ b/frontend/src/app/main/ui/ds/controls/input.cljs
@@ -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]]))
diff --git a/frontend/src/app/main/ui/ds/controls/input.scss b/frontend/src/app/main/ui/ds/controls/input.scss
index dc8ac091c3..d823942e5e 100644
--- a/frontend/src/app/main/ui/ds/controls/input.scss
+++ b/frontend/src/app/main/ui/ds/controls/input.scss
@@ -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%;
diff --git a/frontend/src/app/main/ui/ds/notifications/actionable.cljs b/frontend/src/app/main/ui/ds/notifications/actionable.cljs
new file mode 100644
index 0000000000..8633021b80
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/notifications/actionable.cljs
@@ -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]]))
diff --git a/frontend/src/app/main/ui/ds/notifications/actionable.scss b/frontend/src/app/main/ui/ds/notifications/actionable.scss
new file mode 100644
index 0000000000..745cfbc95f
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/notifications/actionable.scss
@@ -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);
+}
diff --git a/frontend/src/app/main/ui/ds/notifications/actionable.stories.jsx b/frontend/src/app/main/ui/ds/notifications/actionable.stories.jsx
new file mode 100644
index 0000000000..fec1486ed7
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/notifications/actionable.stories.jsx
@@ -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 }) => (
+
+ Message for the notification more info
+
+ ),
+};
+
+export const Default = {};
+
+export const Error = {
+ args: {
+ variant: "error",
+ },
+};
diff --git a/frontend/src/app/main/ui/ds/product/autosaved_milestone.cljs b/frontend/src/app/main/ui/ds/product/autosaved_milestone.cljs
new file mode 100644
index 0000000000..fcb4f788d1
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/autosaved_milestone.cljs
@@ -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}]]))]]))
+
diff --git a/frontend/src/app/main/ui/ds/product/autosaved_milestone.scss b/frontend/src/app/main/ui/ds/product/autosaved_milestone.scss
new file mode 100644
index 0000000000..479fe60877
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/autosaved_milestone.scss
@@ -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);
+}
diff --git a/frontend/src/app/main/ui/ds/product/autosaved_milestone.stories.jsx b/frontend/src/app/main/ui/ds/product/autosaved_milestone.stories.jsx
new file mode 100644
index 0000000000..204185c46a
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/autosaved_milestone.stories.jsx
@@ -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 ;
+ },
+};
+
+export const Default = {};
diff --git a/frontend/src/app/main/ui/ds/product/avatar.cljs b/frontend/src/app/main/ui/ds/product/avatar.cljs
new file mode 100644
index 0000000000..8b8017ccb2
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/avatar.cljs
@@ -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}]]]))
diff --git a/frontend/src/app/main/ui/ds/product/avatar.scss b/frontend/src/app/main/ui/ds/product/avatar.scss
new file mode 100644
index 0000000000..eac040a8eb
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/avatar.scss
@@ -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);
+ }
+}
diff --git a/frontend/src/app/main/ui/ds/product/avatar.stories.jsx b/frontend/src/app/main/ui/ds/product/avatar.stories.jsx
new file mode 100644
index 0000000000..b6e6f72dfa
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/avatar.stories.jsx
@@ -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 }) => ,
+};
+
+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,
+ },
+};
diff --git a/frontend/src/app/main/ui/ds/product/cta.cljs b/frontend/src/app/main/ui/ds/product/cta.cljs
new file mode 100644
index 0000000000..96f7597ded
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/cta.cljs
@@ -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]]))
diff --git a/frontend/src/app/main/ui/ds/product/cta.scss b/frontend/src/app/main/ui/ds/product/cta.scss
new file mode 100644
index 0000000000..f35baf278b
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/cta.scss
@@ -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);
+}
diff --git a/frontend/src/app/main/ui/ds/product/cta.stories.jsx b/frontend/src/app/main/ui/ds/product/cta.stories.jsx
new file mode 100644
index 0000000000..7189a4f828
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/cta.stories.jsx
@@ -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 }) => (
+
+
+ If you’d like to increase this limit, write to us at{" "}
+
+ support@penpot.app
+
+
+
+ ),
+};
+
+export const Default = {};
diff --git a/frontend/src/app/main/ui/ds/product/user_milestone.cljs b/frontend/src/app/main/ui/ds/product/user_milestone.cljs
new file mode 100644
index 0000000000..2764c12d34
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/user_milestone.cljs
@@ -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}]]]]))
+
+
diff --git a/frontend/src/app/main/ui/ds/product/user_milestone.scss b/frontend/src/app/main/ui/ds/product/user_milestone.scss
new file mode 100644
index 0000000000..4505b93aa8
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/user_milestone.scss
@@ -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);
+}
diff --git a/frontend/src/app/main/ui/ds/product/user_milestone.stories.jsx b/frontend/src/app/main/ui/ds/product/user_milestone.stories.jsx
new file mode 100644
index 0000000000..ab55121078
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/product/user_milestone.stories.jsx
@@ -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 ;
+ },
+};
+
+export const Default = {};
diff --git a/frontend/src/app/main/ui/ds/utilities/date.cljs b/frontend/src/app/main/ui/ds/utilities/date.cljs
new file mode 100644
index 0000000000..88041d51ad
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/utilities/date.cljs
@@ -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")]))
diff --git a/frontend/src/app/main/ui/ds/utilities/date.scss b/frontend/src/app/main/ui/ds/utilities/date.scss
new file mode 100644
index 0000000000..1c17695fdd
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/utilities/date.scss
@@ -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));
+}
diff --git a/frontend/src/app/main/ui/ds/utilities/date.stories.jsx b/frontend/src/app/main/ui/ds/utilities/date.stories.jsx
new file mode 100644
index 0000000000..7e8b546da6
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/utilities/date.stories.jsx
@@ -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 }) => ,
+};
+
+export const Default = {};
diff --git a/frontend/src/app/main/ui/notifications.cljs b/frontend/src/app/main/ui/notifications.cljs
index 14c01d3909..6eb0fda862 100644
--- a/frontend/src/app/main/ui/notifications.cljs
+++ b/frontend/src/app/main/ui/notifications.cljs
@@ -39,7 +39,8 @@
inline?
[:& inline-notification
- {:actions (:actions notification)
+ {:accept (:accept notification)
+ :cancel (:cancel notification)
:links (:links notification)
:content (:content notification)}]
diff --git a/frontend/src/app/main/ui/notifications/inline_notification.cljs b/frontend/src/app/main/ui/notifications/inline_notification.cljs
index 85bd77e982..b04b6b03bf 100644
--- a/frontend/src/app/main/ui/notifications/inline_notification.cljs
+++ b/frontend/src/app/main/ui/notifications/inline_notification.cljs
@@ -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)}])])])
diff --git a/frontend/src/app/main/ui/notifications/inline_notification.scss b/frontend/src/app/main/ui/notifications/inline_notification.scss
index 0bbb0d0c01..7deaff1c75 100644
--- a/frontend/src/app/main/ui/notifications/inline_notification.scss
+++ b/frontend/src/app/main/ui/notifications/inline_notification.scss
@@ -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;
}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/versions.cljs b/frontend/src/app/main/ui/workspace/sidebar/versions.cljs
index d60e590115..323d60a433 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/versions.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/versions.cljs
@@ -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")}]]])]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/versions.scss b/frontend/src/app/main/ui/workspace/sidebar/versions.scss
index 4ab4154088..62865de402 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/versions.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/versions.scss
@@ -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);
}