diff --git a/CHANGES.md b/CHANGES.md index c6e9a25e3f..e7669668de 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -55,7 +55,9 @@ - Fix problem with constraints when creating group [Taiga #10455](https://tree.taiga.io/project/penpot/issue/10455) - Fix opening pen with shortcut multiple times breaks toolbar [Taiga #10566](https://tree.taiga.io/project/penpot/issue/10566) - Fix actions when workspace is visited first time [Taiga #10548](https://tree.taiga.io/project/penpot/issue/10548) -- Chat icon overlaps "Show" button in carrousel section [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542) +- Fix chat icon overlaps "Show" button in carrousel section [Taiga #10542](https://tree.taiga.io/project/penpot/issue/10542) +- Fix incorrect handling of background task result (now task rows are properly marked as completed) + ## 2.5.4 diff --git a/backend/src/app/worker/runner.clj b/backend/src/app/worker/runner.clj index eccd58407c..b8b1e84153 100644 --- a/backend/src/app/worker/runner.clj +++ b/backend/src/app/worker/runner.clj @@ -24,6 +24,33 @@ (set! *warn-on-reflection* true) +(def schema:task + [:map {:title "Task"} + [:id ::sm/uuid] + [:queue :string] + [:name :string] + [:created-at ::sm/inst] + [:modified-at ::sm/inst] + [:scheduled-at {:optional true} ::sm/inst] + [:completed-at {:optional true} ::sm/inst] + [:error {:optional true} :string] + [:max-retries :int] + [:retry-num :int] + [:priority :int] + [:status [:enum "scheduled" "completed" "new" "retry" "failed"]] + [:label {:optional true} :string] + [:props :map]]) + +(def schema:result + [:map {:title "TaskResult"} + [:status [:enum "retry" "failed" "completed"]] + [:error {:optional true} [:fn ex/exception?]] + [:inc-by {:optional true} :int] + [:delay {:optional true} :int]]) + +(def valid-task-result? + (sm/validator schema:result)) + (defn- decode-task-row [{:keys [props] :as row}] (cond-> row @@ -51,10 +78,11 @@ :retry (:retry-num task)) (let [tpoint (dt/tpoint) task-fn (wrk/get-task registry (:name task)) - result (if task-fn - (task-fn task) - {:status :completed :task task}) - elapsed (dt/format-duration (tpoint))] + result (when task-fn (task-fn task)) + elapsed (dt/format-duration (tpoint)) + result (if (valid-task-result? result) + result + {:status "completed"})] (when-not task-fn (l/wrn :hint "no task handler found" :name (:name task))) @@ -76,7 +104,7 @@ (if (and (< (:retry-num task) (:max-retries task)) (= ::retry (:type edata))) - (cond-> {:status :retry :task task :error cause} + (cond-> {:status "retry" :error cause} (dt/duration? (:delay edata)) (assoc :delay (:delay edata)) @@ -87,8 +115,8 @@ ::l/context (get-error-context cause task) :cause cause) (if (>= (:retry-num task) (:max-retries task)) - {:status :failed :task task :error cause} - {:status :retry :task task :error cause}))))))) + {:status "failed" :error cause} + {:status "retry" :error cause}))))))) (defn- run-task! [{:keys [::id ::timeout] :as cfg} task-id] @@ -116,12 +144,17 @@ :task-id task-id) :else - (run-task cfg task)))) + (let [result (run-task cfg task)] + (with-meta result + {::task task}))))) (defn- run-worker-loop! [{:keys [::db/pool ::rds/rconn ::timeout ::queue] :as cfg}] - (letfn [(handle-task-retry [{:keys [task error inc-by delay] :or {inc-by 1 delay 1000}}] - (let [explain (ex-message error) + (letfn [(handle-task-retry [{:keys [error inc-by delay] :or {inc-by 1 delay 1000} :as result}] + (let [explain (if (ex/exception? error) + (ex-message error) + (str error)) + task (-> result meta ::task) nretry (+ (:retry-num task) inc-by) now (dt/now) delay (->> (iterate #(* % 2) delay) (take nretry) (last))] @@ -134,8 +167,9 @@ {:id (:id task)}) nil)) - (handle-task-failure [{:keys [task error]}] - (let [explain (ex-message error)] + (handle-task-failure [{:keys [error] :as result}] + (let [task (-> result meta ::task) + explain (ex-message error)] (db/update! pool :task {:error explain :modified-at (dt/now) @@ -143,8 +177,9 @@ {:id (:id task)}) nil)) - (handle-task-completion [{:keys [task]}] - (let [now (dt/now)] + (handle-task-completion [result] + (let [task (-> result meta ::task) + now (dt/now)] (db/update! pool :task {:completed-at now :modified-at now @@ -168,10 +203,11 @@ (process-result [{:keys [status] :as result}] (ex/try! (case status - :retry (handle-task-retry result) - :failed (handle-task-failure result) - :completed (handle-task-completion result) - nil))) + "retry" (handle-task-retry result) + "failed" (handle-task-failure result) + "completed" (handle-task-completion result) + (throw (IllegalArgumentException. + (str "invalid status received: " status)))))) (run-task-loop [task-id] (loop [result (run-task! cfg task-id)] diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 369ffd37f4..f53c806d30 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -1227,18 +1227,15 @@ Will return a value that matches this schema: :none))) (get-active-themes-set-tokens [this] - (let [sets-order (get-ordered-set-names this) - active-themes (get-active-themes this) - order-theme-set (fn [theme] - (filter #(contains? (set (:sets theme)) %) sets-order))] - (reduce - (fn [tokens theme] - (reduce - (fn [tokens' cur] - (merge tokens' (:tokens (get-set this cur)))) - tokens (order-theme-set theme))) - (d/ordered-map) - active-themes))) + (let [theme-set-names (get-active-themes-set-names this) + all-set-names (get-ordered-set-names this) + active-set-names (filter theme-set-names all-set-names) + tokens (reduce (fn [tokens set-name] + (let [set (get-set this set-name)] + (merge tokens (:tokens set)))) + (d/ordered-map) + active-set-names)] + tokens)) (encode-dtcg [this] (let [themes-xform diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc index 6779ac08ee..0b80eed164 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -441,32 +441,225 @@ (t/is (nil? token')) (t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set))))) -(t/deftest list-active-themes-tokens-in-order - (let [tokens-lib (-> (ctob/make-tokens-lib) - (ctob/add-theme (ctob/make-token-theme :name "out-of-order-theme" - ;; Out of order sets in theme - :sets ["unknown-set" "set-b" "set-a"])) - (ctob/set-active-themes #{"/out-of-order-theme"}) +(t/deftest get-ordered-sets + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "group-1/set-a")) + (ctob/add-set (ctob/make-token-set :name "group-1/set-b")) + (ctob/add-set (ctob/make-token-set :name "group-2/set-a")) + (ctob/add-set (ctob/make-token-set :name "group-1/set-c"))) - (ctob/add-set (ctob/make-token-set :name "set-a")) - (ctob/add-token-in-set "set-a" (ctob/make-token :name "set-a-token" - :type :boolean - :value true)) - (ctob/add-set (ctob/make-token-set :name "set-b")) - (ctob/add-token-in-set "set-b" (ctob/make-token :name "set-b-token" - :type :boolean - :value true)) - ;; Ignore this set - (ctob/add-set (ctob/make-token-set :name "inactive-set")) - (ctob/add-token-in-set "inactive-set" (ctob/make-token :name "inactive-set-token" - :type :boolean - :value true))) + ordered-sets (ctob/get-ordered-set-names tokens-lib)] - expected-order (ctob/get-ordered-set-names tokens-lib) - expected-tokens (ctob/get-active-themes-set-tokens tokens-lib) - expected-token-names (mapv key expected-tokens)] - (t/is (= '("set-a" "set-b" "inactive-set") expected-order)) - (t/is (= ["set-a-token" "set-b-token"] expected-token-names)))) + (t/is (= ordered-sets '("group-1/set-a" + "group-1/set-b" + "group-1/set-c" + "group-2/set-a"))))) + +(t/deftest list-active-themes-tokens-no-theme + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "set-a" + :tokens {"token-1" + (ctob/make-token :name "token-1" + :type :border-radius + :value 10) + "token-2" + (ctob/make-token :name "token-2" + :type :border-radius + :value 20)})) + (ctob/add-set (ctob/make-token-set :name "set-b" + :tokens {"token-1" + (ctob/make-token :name "token-1" + :type :border-radius + :value 100) + "token-3" + (ctob/make-token :name "token-3" + :type :border-radius + :value 300)})) + (ctob/add-set (ctob/make-token-set :name "set-c" + :tokens {"token-1" + (ctob/make-token :name "token-1" + :type :border-radius + :value 1000) + "token-2" + (ctob/make-token :name "token-2" + :type :border-radius + :value 2000) + "token-3" + (ctob/make-token :name "token-3" + :type :border-radius + :value 3000) + "token-4" + (ctob/make-token :name "token-4" + :type :border-radius + :value 4000)})) + (ctob/update-theme ctob/hidden-token-theme-group ctob/hidden-token-theme-name + #(ctob/enable-sets % #{"set-a" "set-b"}))) + + tokens (ctob/get-active-themes-set-tokens tokens-lib)] + + (t/is (= (mapv key tokens) ["token-1" "token-2" "token-3"])) + (t/is (= (get-in tokens ["token-1" :value]) 100)) + (t/is (= (get-in tokens ["token-2" :value]) 20)) + (t/is (= (get-in tokens ["token-3" :value]) 300)))) + +(t/deftest list-active-themes-tokens-one-theme + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "set-a" + :tokens {"token-1" + (ctob/make-token :name "token-1" + :type :border-radius + :value 10) + "token-2" + (ctob/make-token :name "token-2" + :type :border-radius + :value 20)})) + (ctob/add-set (ctob/make-token-set :name "set-b" + :tokens {"token-1" + (ctob/make-token :name "token-1" + :type :border-radius + :value 100) + "token-3" + (ctob/make-token :name "token-3" + :type :border-radius + :value 300)})) + (ctob/add-set (ctob/make-token-set :name "set-c" + :tokens {"token-1" + (ctob/make-token :name "token-1" + :type :border-radius + :value 1000) + "token-2" + (ctob/make-token :name "token-2" + :type :border-radius + :value 2000) + "token-3" + (ctob/make-token :name "token-3" + :type :border-radius + :value 3000) + "token-4" + (ctob/make-token :name "token-4" + :type :border-radius + :value 4000)})) + (ctob/add-theme (ctob/make-token-theme :name "single-theme" + :sets #{"set-b" "set-c" "set-a"})) + (ctob/set-active-themes #{"/single-theme"})) + + tokens (ctob/get-active-themes-set-tokens tokens-lib)] + + ;; Note that sets order inside the theme is undefined. What matters is order in that the + ;; sets have been added to the library. + (t/is (= (mapv key tokens) ["token-1" "token-2" "token-3" "token-4"])) + (t/is (= (get-in tokens ["token-1" :value]) 1000)) + (t/is (= (get-in tokens ["token-2" :value]) 2000)) + (t/is (= (get-in tokens ["token-3" :value]) 3000)) + (t/is (= (get-in tokens ["token-4" :value]) 4000)))) + +(t/deftest list-active-themes-tokens-two-themes + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "set-a" + :tokens {"token-1" + (ctob/make-token :name "token-1" + :type :border-radius + :value 10) + "token-2" + (ctob/make-token :name "token-2" + :type :border-radius + :value 20)})) + (ctob/add-set (ctob/make-token-set :name "set-b" + :tokens {"token-1" + (ctob/make-token :name "token-1" + :type :border-radius + :value 100) + "token-3" + (ctob/make-token :name "token-3" + :type :border-radius + :value 300)})) + (ctob/add-set (ctob/make-token-set :name "set-c" + :tokens {"token-1" + (ctob/make-token :name "token-1" + :type :border-radius + :value 1000) + "token-2" + (ctob/make-token :name "token-2" + :type :border-radius + :value 2000) + "token-3" + (ctob/make-token :name "token-3" + :type :border-radius + :value 3000) + "token-4" + (ctob/make-token :name "token-4" + :type :border-radius + :value 4000)})) + (ctob/add-theme (ctob/make-token-theme :name "theme-1" + :sets #{"set-b"})) + (ctob/add-theme (ctob/make-token-theme :name "theme-2" + :sets #{"set-b" "set-a"})) + (ctob/set-active-themes #{"/theme-1" "/theme-2"})) + + tokens (ctob/get-active-themes-set-tokens tokens-lib)] + + ;; Note that themes order is irrelevant. What matters is the union of the active sets + ;; and the order of the sets in the library. + (t/is (= (mapv key tokens) ["token-1" "token-2" "token-3"])) + (t/is (= (get-in tokens ["token-1" :value]) 100)) + (t/is (= (get-in tokens ["token-2" :value]) 20)) + (t/is (= (get-in tokens ["token-3" :value]) 300)))) + +(t/deftest list-active-themes-tokens-bug-taiga-10617 + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "Mode / Dark" + :tokens {"red" + (ctob/make-token :name "red" + :type :color + :value "#700000")})) + (ctob/add-set (ctob/make-token-set :name "Mode / Light" + :tokens {"red" + (ctob/make-token :name "red" + :type :color + :value "#ff0000")})) + (ctob/add-set (ctob/make-token-set :name "Device / Desktop" + :tokens {"border1" + (ctob/make-token :name "border1" + :type :border-radius + :value 30)})) + (ctob/add-set (ctob/make-token-set :name "Device / Mobile" + :tokens {"border1" + (ctob/make-token :name "border1" + :type :border-radius + :value 50)})) + (ctob/add-theme (ctob/make-token-theme :group "App" + :name "Mobile" + :sets #{"Mode / Dark" "Device / Mobile"})) + (ctob/add-theme (ctob/make-token-theme :group "App" + :name "Web" + :sets #{"Mode / Dark" "Mode / Light" "Device / Desktop"})) + (ctob/add-theme (ctob/make-token-theme :group "Brand" + :name "Brand A" + :sets #{"Mode / Dark" "Mode / Light" "Device / Desktop" "Device / Mobile"})) + (ctob/add-theme (ctob/make-token-theme :group "Brand" + :name "Brand B" + :sets #{})) + (ctob/set-active-themes #{"App/Web" "Brand/Brand A"})) + + tokens (ctob/get-active-themes-set-tokens tokens-lib)] + + (t/is (= (mapv key tokens) ["red" "border1"])) + (t/is (= (get-in tokens ["red" :value]) "#ff0000")) + (t/is (= (get-in tokens ["border1" :value]) 50)))) + +(t/deftest list-active-themes-tokens-no-tokens + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "set-a"))) + + tokens (ctob/get-active-themes-set-tokens tokens-lib)] + + (t/is (empty? tokens)))) + +(t/deftest list-active-themes-tokens-no-sets + (let [tokens-lib (ctob/make-tokens-lib) + tokens (ctob/get-active-themes-set-tokens tokens-lib)] + + (t/is (empty? tokens)))) (t/deftest sets-at-path-active-state (let [tokens-lib (-> (ctob/make-tokens-lib) diff --git a/frontend/resources/images/features/2.6-bubbles.gif b/frontend/resources/images/features/2.6-bubbles.gif new file mode 100644 index 0000000000..55a694fee1 Binary files /dev/null and b/frontend/resources/images/features/2.6-bubbles.gif differ diff --git a/frontend/resources/images/features/2.6-slide-0.png b/frontend/resources/images/features/2.6-slide-0.png new file mode 100644 index 0000000000..abea70922a Binary files /dev/null and b/frontend/resources/images/features/2.6-slide-0.png differ diff --git a/frontend/resources/images/features/2.6-tokens-1.gif b/frontend/resources/images/features/2.6-tokens-1.gif new file mode 100644 index 0000000000..8922811556 Binary files /dev/null and b/frontend/resources/images/features/2.6-tokens-1.gif differ diff --git a/frontend/resources/images/features/2.6-tokens-2.gif b/frontend/resources/images/features/2.6-tokens-2.gif new file mode 100644 index 0000000000..7da42111cb Binary files /dev/null and b/frontend/resources/images/features/2.6-tokens-2.gif differ diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs index a7cd6b7f28..52e728437e 100644 --- a/frontend/src/app/main/ui/releases.cljs +++ b/frontend/src/app/main/ui/releases.cljs @@ -32,6 +32,7 @@ [app.main.ui.releases.v2-3] [app.main.ui.releases.v2-4] [app.main.ui.releases.v2-5] + [app.main.ui.releases.v2-6] [app.util.object :as obj] [app.util.timers :as tm] [rumext.v2 :as mf])) @@ -96,4 +97,4 @@ (defmethod rc/render-release-notes "0.0" [params] - (rc/render-release-notes (assoc params :version "2.5"))) + (rc/render-release-notes (assoc params :version "2.6"))) diff --git a/frontend/src/app/main/ui/releases/v2_6.cljs b/frontend/src/app/main/ui/releases/v2_6.cljs new file mode 100644 index 0000000000..9d47c870f7 --- /dev/null +++ b/frontend/src/app/main/ui/releases/v2_6.cljs @@ -0,0 +1,169 @@ +;; 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.releases.v2-6 + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data.macros :as dm] + [app.main.ui.releases.common :as c] + [rumext.v2 :as mf])) + +(defmethod c/render-release-notes "2.6" + [{:keys [slide klass next finish navigate version]}] + (mf/html + (case slide + :start + [:div {:class (stl/css-case :modal-overlay true)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.6-slide-0.png" + :class (stl/css :start-image) + :border "0" + :alt "Design Tokens make their debut in Penpot!"}] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "What’s new in Penpot?"] + + [:div {:class (stl/css :version-tag)} + (dm/str "Version " version)]] + + [:div {:class (stl/css :features-block)} + [:span {:class (stl/css :feature-title)} + "Design Tokens make their debut in Penpot!"] + + [:p {:class (stl/css :feature-content)} + "Penpot is the first design tool to integrate native design + tokens—a single source of truth to improve efficiency and + collaboration between product design and development."] + + [:p {:class (stl/css :feature-content)} + "But that’s not all—we’ve also tackled improvements, bug fixes and optimizations."] + + [:p {:class (stl/css :feature-content)} + "Let’s dive in!"]] + + [:div {:class (stl/css :navigation)} + [:button {:class (stl/css :next-btn) + :on-click next} "Continue"]]]]]] + + 0 + [:div {:class (stl/css-case :modal-overlay true)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.6-tokens-1.gif" + :class (stl/css :start-image) + :border "0" + :alt "Manage brands and themes across your design systems"}] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "Manage brands and themes across your design systems"]] + + [:div {:class (stl/css :feature)} + [:p {:class (stl/css :feature-content)} + "Create and manage different token types—Color, Opacity, + Border Radius, Dimension, Sizing, Spacing, Rotation, and + Stroke. And this is just the beginning—more token types are + on the way!"] + + [:p {:class (stl/css :feature-content)} + "Add values to your tokens, including references to other + tokens (aliases) and even math operations to keep things + dynamic and flexible."] + + [:p {:class (stl/css :feature-content)} + "Use Themes and Sets for an efficient way to manage your + design system across multiple dimensions—whether it’s + brand, color schemes, devices, density, or anything else + your product needs."]] + + [:div {:class (stl/css :navigation)} + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 3}] + + [:button {:on-click next + :class (stl/css :next-btn)} "Continue"]]]]]] + + 1 + [:div {:class (stl/css-case :modal-overlay true)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.6-tokens-2.gif" + :class (stl/css :start-image) + :border "0" + :alt "Open Source design tokens format"}] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "Open Source design tokens format"]] + + [:div {:class (stl/css :feature)} + [:p {:class (stl/css :feature-content)} + "Penpot adopts the W3C Design Tokens Community Group (DTCG) + standard, ensuring maximum compatibility with a wide range + of tools and technologies."] + + [:p {:class (stl/css :feature-content)} + "With Penpot’s standardized design tokens format, you can + easily reuse and sync tokens across different platforms, + workflows, and disciplines. Import your existing tokens + into Penpot—or export them for use anywhere else. Seamless + interoperability by design through Open Source."]] + + [:div {:class (stl/css :navigation)} + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 3}] + + [:button {:on-click next + :class (stl/css :next-btn)} "Continue"]]]]]] + + 2 + [:div {:class (stl/css-case :modal-overlay true)} + [:div.animated {:class klass} + [:div {:class (stl/css :modal-container)} + [:img {:src "images/features/2.6-bubbles.gif" + :class (stl/css :start-image) + :border "0" + :alt "Comments grouped by zoom level"}] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-header)} + [:h1 {:class (stl/css :modal-title)} + "Comments grouped by zoom level"]] + [:div {:class (stl/css :feature)} + + [:p {:class (stl/css :feature-content)} + "When collaborating on files, feedback can quickly become + dense and overwhelming, turning what should be information + into visual noise. Now, comments are grouped based on your + zoom level, giving the right level of visibility and making + navigating feedback easier."] + + [:p {:class (stl/css :feature-content)} + "When you’re zoomed out, comments are grouped to reduce + clutter and keep your workspace clean. As you zoom in, the + groups expand, revealing individual comments in + context. This makes navigating feedback much smoother, + especially in complex designs with lots of discussion."]] + + [:div {:class (stl/css :navigation)} + + [:& c/navigation-bullets + {:slide slide + :navigate navigate + :total 3}] + + [:button {:on-click finish + :class (stl/css :next-btn)} "Let's go"]]]]]]))) + diff --git a/frontend/src/app/main/ui/releases/v2_6.scss b/frontend/src/app/main/ui/releases/v2_6.scss new file mode 100644 index 0000000000..dd1b81c82b --- /dev/null +++ b/frontend/src/app/main/ui/releases/v2_6.scss @@ -0,0 +1,102 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-container { + display: grid; + grid-template-columns: $s-324 1fr; + height: $s-500; + width: $s-888; + border-radius: $br-8; + background-color: var(--modal-background-color); + border: $s-2 solid var(--modal-border-color); +} + +.start-image { + width: $s-324; + border-radius: $br-8 0 0 $br-8; +} + +.modal-content { + padding: $s-40; + display: grid; + grid-template-rows: auto 1fr $s-32; + gap: $s-24; + + a { + color: var(--button-primary-background-color-rest); + } +} + +.modal-header { + display: grid; + gap: $s-8; +} + +.version-tag { + @include flexCenter; + @include headlineSmallTypography; + height: $s-32; + width: $s-96; + background-color: var(--communication-tag-background-color); + color: var(--communication-tag-foreground-color); + border-radius: $br-8; +} + +.modal-title { + @include headlineLargeTypography; + color: var(--modal-title-foreground-color); +} + +.features-block { + display: flex; + flex-direction: column; + gap: $s-16; + width: $s-440; +} + +.feature { + display: flex; + flex-direction: column; + gap: $s-8; +} + +.feature-title { + @include bodyLargeTypography; + color: var(--modal-title-foreground-color); +} + +.feature-content { + @include bodyMediumTypography; + margin: 0; + color: var(--modal-text-foreground-color); +} + +.feature-list { + @include bodyMediumTypography; + color: var(--modal-text-foreground-color); + list-style: disc; + display: grid; + gap: $s-8; +} + +.navigation { + width: 100%; + display: grid; + grid-template-areas: "bullets button"; +} + +.next-btn { + @extend .button-primary; + width: $s-100; + justify-self: flex-end; + grid-area: button; +}