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