From f891f4237db73e22c9280a42ad27449f32ab6fca Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 16 Jun 2026 17:17:22 +0200 Subject: [PATCH] WIP 2 --- frontend/src/app/main/ui.cljs | 3 +- frontend/src/app/main/ui/routes.cljs | 3 +- frontend/src/app/main/ui/settings.cljs | 6 +- .../src/app/main/ui/settings/shortcuts.cljs | 179 +++++++ .../src/app/main/ui/settings/shortcuts.scss | 17 + .../src/app/main/ui/settings/sidebar.cljs | 9 + frontend/src/app/main/ui/shortcuts.cljs | 281 +++++++++++ frontend/src/app/main/ui/shortcuts.scss | 122 +++++ .../main/ui/workspace/sidebar/shortcuts.cljs | 452 +----------------- .../main/ui/workspace/sidebar/shortcuts.scss | 119 +---- 10 files changed, 641 insertions(+), 550 deletions(-) create mode 100644 frontend/src/app/main/ui/settings/shortcuts.cljs create mode 100644 frontend/src/app/main/ui/settings/shortcuts.scss create mode 100644 frontend/src/app/main/ui/shortcuts.cljs create mode 100644 frontend/src/app/main/ui/shortcuts.scss diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 157ae24d9b..8656122c4d 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -200,7 +200,8 @@ :settings-feedback :settings-subscription :settings-integrations - :settings-notifications) + :settings-notifications + :settings-shortcuts) (let [params (get params :query) error-report-id (some-> params :error-report-id uuid/parse*)] [:? [:> settings-page* diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index bbac87d452..58ecf0319b 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -41,7 +41,8 @@ ["/options" :settings-options] ["/subscriptions" :settings-subscription] ["/integrations" :settings-integrations] - ["/notifications" :settings-notifications]] + ["/notifications" :settings-notifications] + ["/shortcuts" :settings-shortcuts]] ["/frame-preview" :frame-preview] diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index 57665fc9d8..5801d8e98f 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -21,6 +21,7 @@ [app.main.ui.settings.options :refer [options-page*]] [app.main.ui.settings.password :refer [password-page*]] [app.main.ui.settings.profile :refer [profile-page*]] + [app.main.ui.settings.shortcuts :refer [shortcuts-page*]] [app.main.ui.settings.sidebar :refer [sidebar*]] [app.main.ui.settings.subscription :refer [subscription-page*]] [app.util.i18n :as i18n :refer [tr]] @@ -77,7 +78,10 @@ [:> integrations-page*] :settings-notifications - [:> notifications-page* {:profile profile}])]]]])) + [:> notifications-page* {:profile profile}] + + :settings-shortcuts + [:> shortcuts-page* {:profile profile}])]]]])) (mf/defc settings-page* {::mf/lazy-load true} diff --git a/frontend/src/app/main/ui/settings/shortcuts.cljs b/frontend/src/app/main/ui/settings/shortcuts.cljs new file mode 100644 index 0000000000..9e57c2da91 --- /dev/null +++ b/frontend/src/app/main/ui/settings/shortcuts.cljs @@ -0,0 +1,179 @@ +(ns app.main.ui.settings.shortcuts + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.main.data.dashboard.shortcuts] + [app.main.data.modal :as modal] + [app.main.data.shortcuts :as ds] + [app.main.data.viewer.shortcuts] + [app.main.data.workspace.path.shortcuts] + [app.main.data.workspace.shortcuts] + [app.main.data.workspace.shortcuts.customize :as customize] + [app.main.store :as st] + [app.main.ui.ds.foundations.typography :as t] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]] + [app.main.ui.shortcuts :as ss] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +(def ^:private workspace-shortcuts-raw + (d/deep-merge app.main.data.workspace.path.shortcuts/shortcuts + app.main.data.workspace.shortcuts/shortcuts)) + +(mf/defc shortcuts-list* + [{:keys [profile workspace-sc dashboard-sc viewer-sc]}] + (let [open-sections* (mf/use-state [[1]]) + open-sections (deref open-sections*) + custom-shortcuts (get-in profile [:props :custom-shortcuts]) + + workspace-translated (->> workspace-sc + (ss/add-translation :sc) + (into {})) + dashboard-translated (->> dashboard-sc + (ss/add-translation :sc) + (into {})) + viewer-translated (->> viewer-sc + (ss/add-translation :sc) + (into {})) + + filter-term (mf/use-state "") + + {:keys [all-shortcuts]} + (ss/build-all-shortcuts workspace-translated dashboard-translated viewer-translated) + + section-has-content? + (fn [section] + (let [children (:children section)] + (if (and (= (count children) 1) (contains? children :none)) + (seq (:children (:none children))) + (seq children)))) + + all-shortcuts (into {} (filter (fn [[_ v]] (section-has-content? v)) all-shortcuts)) + + on-edit-shortcut + (mf/use-callback + (mf/deps custom-shortcuts workspace-sc) + (fn [shortcut-key] + (let [default-command (:command (get workspace-shortcuts-raw shortcut-key)) + current-command (or (get custom-shortcuts shortcut-key) default-command)] + (st/emit! (modal/show :shortcut-edit + {:shortcut-key shortcut-key + :shortcut-name (ss/translation-keyname :sc shortcut-key) + :current-command current-command + :default-command default-command + :all-shortcuts all-shortcuts}))))) + + on-reset-shortcut + (mf/use-callback + (fn [shortcut-key] + (st/emit! (customize/reset-custom-shortcut shortcut-key)))) + manage-sections + (fn [item] + (fn [event] + (dom/stop-propagation event) + (let [is-present? (some #(= % item) open-sections) + new-value (if is-present? + (filterv (fn [element] (not= element item)) open-sections) + (conj open-sections item))] + (reset! open-sections* new-value))))] + + (for [section all-shortcuts] + (let [[section-key _] section] + [:> ss/shortcut-section* {:key (name section-key) + :section section + :manage-sections manage-sections + :open-sections open-sections* + :filter-term filter-term + :editable? true + :custom-shortcuts custom-shortcuts + :on-edit on-edit-shortcut + :on-reset on-reset-shortcut}])))) + +(mf/defc all-shortcuts-section* + [{:keys [profile]}] + (let [custom-shortcuts (get-in profile [:props :custom-shortcuts]) + workspace-shortcuts-custom (ds/apply-custom-overrides workspace-shortcuts-raw custom-shortcuts)] + [:div {:class (stl/css :shortcuts-section)} + [:> shortcuts-list* {:profile profile + :workspace-sc workspace-shortcuts-custom + :dashboard-sc app.main.data.dashboard.shortcuts/shortcuts + :viewer-sc app.main.data.viewer.shortcuts/shortcuts}]])) + +(mf/defc personalized-shortcuts-section* + [{:keys [profile]}] + (let [custom-shortcuts (get-in profile [:props :custom-shortcuts]) + workspace-shortcuts-custom (ds/apply-custom-overrides workspace-shortcuts-raw custom-shortcuts) + personalized (select-keys workspace-shortcuts-custom (keys custom-shortcuts))] + [:div {:class (stl/css :shortcuts-section)} + (if (seq personalized) + [:> shortcuts-list* {:profile profile + :workspace-sc personalized + :dashboard-sc {} + :viewer-sc {}}] + [:p "There are no personalized shortcuts."])])) + +(mf/defc not-assigned-shortcuts-section* + [{:keys [profile]}] + (let [custom-shortcuts (get-in profile [:props :custom-shortcuts]) + workspace-shortcuts-custom (ds/apply-custom-overrides workspace-shortcuts-raw custom-shortcuts) + not-assigned (into {} (filter (fn [[_ v]] + (empty? (:command v))) + workspace-shortcuts-custom))] + [:div {:class (stl/css :shortcuts-section)} + (if (seq not-assigned) + [:> shortcuts-list* {:profile profile + :workspace-sc not-assigned + :dashboard-sc {} + :viewer-sc {}}] + [:p "There are no not-assigned shortcuts."])])) + + + +(mf/defc shortcuts-page* + [{:keys [profile]}] + (let [section* (mf/use-state :all) + section (deref section*) + + tabs + (mf/with-memo [] + [{:label "All shortcuts" + :id "all"} + {:label "Personalized shortcuts" + :data-testid "personalized" + :id "personalized"} + {:label "Not assigned shortcuts" + :data-testid "not-assigned" + :id "not-assigned"}]) + + handle-change-tab + (mf/use-fn + (mf/deps) + (fn [new-section] + (reset! section* (keyword new-section))))] + + + [:section {:class (stl/css :shortcuts-page) + :aria-label "Shortcuts page"} + [:> heading* {:level 1 + :typography t/title-large + :class (stl/css :color-primary)} + "Shortcuts page"] + + [:div {:class (stl/css :shortcuts-content)} + [:p "search-bar"] + [:button "restore all"] + [:p "This is the shortcuts page content."] + [:> tab-switcher* {:tabs tabs + :selected (name section) + :on-change handle-change-tab + :class (stl/css :viewer-tab-switcher)} + (case section + :all + [:> all-shortcuts-section* {:profile profile}] + + :personalized + [:> personalized-shortcuts-section* {:profile profile}] + + :not-assigned + [:> not-assigned-shortcuts-section* {:profile profile}])]]])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/settings/shortcuts.scss b/frontend/src/app/main/ui/settings/shortcuts.scss new file mode 100644 index 0000000000..fae5f909b1 --- /dev/null +++ b/frontend/src/app/main/ui/settings/shortcuts.scss @@ -0,0 +1,17 @@ +// 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 Sucursal en España SL + +.shortcuts-page { + height: 100%; +} + +.shortcuts-content { + padding: var(--sp-xl); +} + +.shortcuts-section { + margin-bottom: var(--sp-lg); +} diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index 94c634004f..e7beee8a55 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -48,6 +48,9 @@ (def ^:private go-settings-notifications #(st/emit! (rt/nav :settings-notifications))) +(def ^:private go-settings-shortcuts + #(st/emit! (rt/nav :settings-shortcuts))) + (defn- show-release-notes [event] (let [version (:main cf/version)] @@ -66,6 +69,7 @@ subscription? (= section :settings-subscription) integrations? (= section :settings-integrations) notifications? (= section :settings-notifications) + shortcuts? (= section :settings-shortcuts) team-id (or (dtm/get-last-team-id) (:default-team-id profile)) @@ -99,6 +103,11 @@ :settings-item true) :on-click go-settings-notifications} [:span {:class (stl/css :element-title)} (tr "labels.notifications")]] + + [:li {:class (stl/css-case :current shortcuts? + :settings-item true) + :on-click go-settings-shortcuts} + [:span {:class (stl/css :element-title)} "shortcuts"]] [:li {:class (stl/css-case :current options? :settings-item true) diff --git a/frontend/src/app/main/ui/shortcuts.cljs b/frontend/src/app/main/ui/shortcuts.cljs new file mode 100644 index 0000000000..345a1f7da6 --- /dev/null +++ b/frontend/src/app/main/ui/shortcuts.cljs @@ -0,0 +1,281 @@ +;; 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 Sucursal en España SL + +(ns app.main.ui.shortcuts + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.config :as cf] + [app.main.data.shortcuts :as ds] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [app.util.strings :refer [matches-search]] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn translation-keyname + [type keyname] + (let [translat-pre (case type + :sc "shortcuts." + :sec "shortcut-section." + :sub-sec "shortcut-subsection.")] + (tr (str translat-pre (d/name keyname))))) + +(defn add-translation + [type item] + (map (fn [[k v]] [k (assoc v :translation (translation-keyname type k))]) item)) + +(defn shortcuts->subsections + [shortcuts] + (let [subsections (into #{} (mapcat :subsections) (vals shortcuts)) + get-sc-by-subsection + (fn [subsection [k v]] + (when (some #(= subsection %) (:subsections v)) {k v})) + reduce-sc + (fn [acc subsection] + (let [shortcuts-by-subsection (into {} (keep (partial get-sc-by-subsection subsection) shortcuts))] + (assoc acc subsection {:children shortcuts-by-subsection})))] + (reduce reduce-sc {} subsections))) + +(defn build-all-shortcuts + [workspace-sc dashboard-sc viewer-sc] + (let [walk (fn walk [element parent-id] + (if (nil? element) + element + (let [rec-fn (fn [index [k item]] + (let [item-id (if (nil? parent-id) + [index] + (conj parent-id index))] + [k (assoc item :id item-id :children (walk (:children item) item-id))]))] + (into {} (map-indexed (partial rec-fn) element))))) + ws-subs (->> (shortcuts->subsections workspace-sc) + (add-translation :sub-sec) + (into {})) + db-subs (->> (shortcuts->subsections dashboard-sc) + (add-translation :sub-sec) + (into {})) + vw-subs (->> (shortcuts->subsections viewer-sc) + (add-translation :sub-sec) + (into {})) + basics (into {} (concat (:children (:basics ws-subs)) + (:children (:basics db-subs)) + (:children (:basics vw-subs)))) + ws-subs (dissoc ws-subs :basics) + db-subs (dissoc db-subs :basics) + vw-subs (dissoc vw-subs :basics) + all {:basics {:id [1] + :children {:none {:children basics}} + :translation (tr "shortcut-section.basics")} + :workspace {:id [2] + :children ws-subs + :translation (tr "shortcut-section.workspace")} + :dashboard {:id [3] + :children db-subs + :translation (tr "shortcut-section.dashboard")} + :viewer {:id [4] + :children vw-subs + :translation (tr "shortcut-section.viewer")}} + all (walk all nil) + all-sc-names (map #(translation-keyname :sc %) + (concat (keys workspace-sc) + (keys dashboard-sc) + (keys viewer-sc))) + all-sub-names (map #(translation-keyname :sub-sec %) + (concat (keys ws-subs) + (keys db-subs) + (keys vw-subs))) + all-section-names (map #(translation-keyname :sec %) (keys all))] + {:all-shortcuts all + :all-sc-names all-sc-names + :all-sub-names all-sub-names + :all-section-names all-section-names})) + +(mf/defc converted-chars* + [{:keys [char command]}] + (let [modified-keys {:up ds/up-arrow + :down ds/down-arrow + :left ds/left-arrow + :right ds/right-arrow + :plus "+"} + macos-keys {:command "\u2318" + :option "\u2325" + :alt "\u2325" + :delete "\u232B" + :del "\u232B" + :shift "\u21E7" + :control "\u2303" + :esc "\u238B" + :enter "\u23CE"} + is-macos? (cf/check-platform? :macos) + char (if (contains? modified-keys (keyword char)) ((keyword char) modified-keys) char) + char (if (and is-macos? (contains? macos-keys (keyword char))) ((keyword char) macos-keys) char) + unique-key (str (d/name command) "-" char)] + [:span {:class (stl/css :key) + :key unique-key} char])) + +(mf/defc shortcuts-keys* + [{:keys [content command]}] + (let [managed-list (if (coll? content) + content + (conj () content)) + chars-list (map ds/split-sc managed-list) + last-element (last chars-list) + short-char-list (if (= 1 (count chars-list)) + chars-list + (drop-last chars-list)) + penultimate (last short-char-list)] + [:span {:class (stl/css :keys)} + (for [chars short-char-list] + [:* {:key (str/join chars)} + (for [char chars] + [:> converted-chars* {:key (dm/str char "-" (name command)) + :char char + :command command}]) + (when (not= chars penultimate) [:span {:class (stl/css :space)} ","])]) + (when (not= last-element penultimate) + [:* + [:span {:class (stl/css :space)} (tr "shortcuts.or")] + (for [char last-element] + [:> converted-chars* {:key (dm/str char "-" (name command)) + :char char + :command command}])])])) + +(mf/defc shortcut-row* + [{:keys [elements filter-term is-match-section is-match-subsection + editable? custom-shortcuts on-edit on-reset]}] + (let [shortcut-name (keys elements) + shortcut-translations (map #(translation-keyname :sc %) shortcut-name) + match-shortcut? (some #(matches-search % @filter-term) shortcut-translations) + filtered (if (and (or is-match-section is-match-subsection) (not match-shortcut?)) + shortcut-translations + (filter #(matches-search % @filter-term) shortcut-translations)) + sorted-filtered (sort filtered)] + + [:ul {:class (stl/css :sub-menu)} + (for [command-translate sorted-filtered] + (let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements)) + [command comand-info] sc-by-translate + content (or (:show-command comand-info) (:command comand-info)) + customized? (and editable? (contains? custom-shortcuts command))] + [:li {:class (stl/css-case :shortcuts-name true + :customized customized?) + :key command-translate} + [:span {:class (stl/css :command-name)} + command-translate] + [:div {:class (stl/css :shortcut-actions)} + [:> shortcuts-keys* {:content content + :command command}] + (when editable? + [:div {:class (stl/css :edit-buttons)} + (when customized? + [:> icon-button* {:variant "ghost" + :aria-label (tr "shortcuts.reset") + :on-click (fn [e] + (dom/stop-propagation e) + (on-reset command)) + :icon i/reload + :icon-size "s"}]) + [:> icon-button* {:variant "ghost" + :aria-label (tr "shortcuts.edit") + :on-click (fn [e] + (dom/stop-propagation e) + (on-edit command)) + :icon i/curve + :icon-size "s"}]])]]))])) + +(mf/defc section-title* + [{:keys [name is-visible is-sub]}] + [:div {:class (if is-sub + (stl/css :subsection-title) + (stl/css :section-title))} + [:> icon* {:icon-id (if is-visible i/arrow-down i/arrow-right) + :size "s"}] + [:span {:class (if is-sub + (stl/css :subsection-name) + (stl/css :section-name))} name]]) + +(mf/defc shortcut-subsection* + [{:keys [subsections manage-sections filter-term is-match-section open-sections + editable? custom-shortcuts on-edit on-reset]}] + (let [subsections-names (keys subsections) + subsection-translations (if (= :none (first subsections-names)) + (map #(translation-keyname :sc %) subsections-names) + (map #(translation-keyname :sub-sec %) subsections-names)) + sorted-translations (sort subsection-translations)] + (if (= :none (first subsections-names)) + (let [basic-shortcuts (:none subsections)] + [:> shortcut-row* {:elements (:children basic-shortcuts) + :filter-term filter-term + :is-match-section is-match-section + :is-match-subsection true + :editable? editable? + :custom-shortcuts custom-shortcuts + :on-edit on-edit + :on-reset on-reset}]) + + [:ul {:class (stl/css :subsection-menu)} + (for [sub-translated sorted-translations] + (let [sub-by-translate (first (filter #(= (:translation (second %)) sub-translated) subsections)) + [sub-name sub-info] sub-by-translate + visible? (some #(= % (:id sub-info)) @open-sections) + match-subsection? (matches-search (translation-keyname :sub-sec sub-name) @filter-term) + shortcut-names (map #(translation-keyname :sc %) (keys (:children sub-info))) + match-shortcuts? (some #(matches-search % @filter-term) shortcut-names)] + (when (or match-subsection? match-shortcuts? is-match-section) + [:li {:key sub-translated + :on-click (manage-sections (:id sub-info))} + [:> section-title* {:name sub-translated + :is-visible visible? + :is-sub true}] + + [:div {:style {:display (if visible? "initial" "none")}} + [:> shortcut-row* {:elements (:children sub-info) + :filter-term filter-term + :is-match-section is-match-section + :is-match-subsection match-subsection? + :editable? editable? + :custom-shortcuts custom-shortcuts + :on-edit on-edit + :on-reset on-reset}]]])))]))) + +(mf/defc shortcut-section* + [{:keys [section manage-sections open-sections filter-term + editable? custom-shortcuts on-edit on-reset]}] + (let [[section-key section-info] section + section-id (:id section-info) + section-translation (translation-keyname :sec section-key) + match-section? (matches-search section-translation @filter-term) + subsections (:children section-info) + subs-names (keys subsections) + subs-bodys (reduce #(conj %1 (:children (%2 subsections))) {} subs-names) + sub-trans (map #(if (= "none" (d/name %)) + nil + (translation-keyname :sub-sec %)) subs-names) + match-subsection? (some #(matches-search % @filter-term) sub-trans) + translations (map #(translation-keyname :sc %) (keys subs-bodys)) + match-shortcut? (some #(matches-search % @filter-term) translations) + visible? (some #(= % section-id) @open-sections)] + + (when (or match-section? match-subsection? match-shortcut?) + [:div {:class (stl/css :section) + :on-click (manage-sections section-id)} + [:> section-title* {:name section-translation + :is-visible visible? + :is-sub false}] + + [:div {:style {:display (if visible? "initial" "none")}} + [:> shortcut-subsection* {:subsections subsections + :open-sections open-sections + :manage-sections manage-sections + :is-match-section match-section? + :filter-term filter-term + :editable? editable? + :custom-shortcuts custom-shortcuts + :on-edit on-edit + :on-reset on-reset}]]]))) diff --git a/frontend/src/app/main/ui/shortcuts.scss b/frontend/src/app/main/ui/shortcuts.scss new file mode 100644 index 0000000000..3f3f57b3aa --- /dev/null +++ b/frontend/src/app/main/ui/shortcuts.scss @@ -0,0 +1,122 @@ +// 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 Sucursal en España SL + +@use "refactor/common-refactor.scss" as deprecated; + +.section { + margin: 0; +} + +.shortcuts-list { + display: flex; + flex-direction: column; + height: 100%; + padding: deprecated.$s-12; + overflow-y: auto; + font-size: deprecated.$fs-12; + color: var(--title-foreground-color); +} + +.section-title, +.subsection-title { + @include deprecated.uppercase-title-typography; + + display: flex; + align-items: center; + margin: 0; + padding: deprecated.$s-8 0; + cursor: pointer; + + .subsection-name, + .section-name { + padding-left: deprecated.$s-4; + } + + &:hover { + color: var(--title-foreground-color-hover); + } +} + +.subsection-title { + text-transform: none; + padding-left: deprecated.$s-12; +} + +.subsection-menu { + margin-bottom: deprecated.$s-4; +} + +.sub-menu { + margin-bottom: deprecated.$s-4; + + .shortcuts-name { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: deprecated.$s-32; + padding: deprecated.$s-6; + margin-bottom: deprecated.$s-4; + border-radius: deprecated.$br-8; + background-color: var(--pill-background-color); + + .command-name { + @include deprecated.body-small-typography; + + margin-left: deprecated.$s-2; + color: var(--pill-foreground-color); + flex: 1; + min-width: 0; + } + + .shortcut-actions { + display: flex; + align-items: center; + gap: deprecated.$s-2; + } + + .edit-buttons { + display: flex; + align-items: center; + opacity: 0; + transition: opacity 0.15s ease; + } + + &:hover .edit-buttons { + opacity: 1; + } + + &.customized { + background-color: var(--pill-background-color-hover, var(--pill-background-color)); + + .edit-buttons { + opacity: 1; + } + } + + .keys { + @include deprecated.flex-center; + + gap: deprecated.$s-2; + color: var(--pill-foreground-color); + + .key { + @include deprecated.body-small-typography; + @include deprecated.flex-center; + + text-transform: capitalize; + height: deprecated.$s-20; + padding: deprecated.$s-2 deprecated.$s-6; + border-radius: deprecated.$s-6; + background-color: var(--menu-shortcut-background-color); + } + + .space { + margin: 0 deprecated.$s-2; + } + } + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs index 49e7f1ad5e..3cc1a5f8cc 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs @@ -9,7 +9,6 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.config :as cf] [app.main.data.dashboard.shortcuts] [app.main.data.modal :as modal] [app.main.data.shortcuts :as ds] @@ -22,385 +21,16 @@ [app.main.store :as st] [app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] - [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] + [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.product.panel-title :refer [panel-title*]] + [app.main.ui.shortcuts :as ss] [app.main.ui.workspace.sidebar.shortcuts.edit-modal] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.strings :refer [matches-search]] [clojure.set :as set] - [clojure.string] - [cuerdas.core :as str] [rumext.v2 :as mf])) -(mf/defc converted-chars* - [{:keys [char command]}] - (let [modified-keys {:up ds/up-arrow - :down ds/down-arrow - :left ds/left-arrow - :right ds/right-arrow - :plus "+"} - macos-keys {:command "\u2318" - :option "\u2325" - :alt "\u2325" - :delete "\u232B" - :del "\u232B" - :shift "\u21E7" - :control "\u2303" - :esc "\u238B" - :enter "\u23CE"} - is-macos? (cf/check-platform? :macos) - char (if (contains? modified-keys (keyword char)) ((keyword char) modified-keys) char) - char (if (and is-macos? (contains? macos-keys (keyword char))) ((keyword char) macos-keys) char) - unique-key (str (d/name command) "-" char)] - [:span {:class (stl/css :key) - :key unique-key} char])) - -(defn translation-keyname - [type keyname] - ;; Execution time translation strings: - (comment - (tr "shortcut-subsection.alignment") - (tr "shortcut-subsection.edit") - (tr "shortcut-subsection.general-dashboard") - (tr "shortcut-subsection.general-viewer") - (tr "shortcut-subsection.main-menu") - (tr "shortcut-subsection.modify-layers") - (tr "shortcut-subsection.navigation-dashboard") - (tr "shortcut-subsection.navigation-viewer") - (tr "shortcut-subsection.navigation-workspace") - (tr "shortcut-subsection.panels") - (tr "shortcut-subsection.path-editor") - (tr "shortcut-subsection.shape") - (tr "shortcut-subsection.text-editor") - (tr "shortcut-subsection.tools") - (tr "shortcut-subsection.zoom-viewer") - (tr "shortcut-subsection.zoom-workspace") - (tr "shortcuts.add-comment") - (tr "shortcuts.add-node") - (tr "shortcuts.align-bottom") - (tr "shortcuts.align-center") - (tr "shortcuts.align-hcenter") - (tr "shortcuts.align-justify") - (tr "shortcuts.align-left") - (tr "shortcuts.align-right") - (tr "shortcuts.align-top") - (tr "shortcuts.align-vcenter") - (tr "shortcuts.artboard-selection") - (tr "shortcuts.bold") - (tr "shortcuts.bool-difference") - (tr "shortcuts.bool-exclude") - (tr "shortcuts.bool-intersection") - (tr "shortcuts.bool-union") - (tr "shortcuts.bring-back") - (tr "shortcuts.bring-backward") - (tr "shortcuts.bring-forward") - (tr "shortcuts.bring-front") - (tr "shortcuts.clear-undo") - (tr "shortcuts.copy") - (tr "shortcuts.copy-link") - (tr "shortcuts.create-component-variant") - (tr "shortcuts.create-new-project") - (tr "shortcuts.cut") - (tr "shortcuts.decrease-zoom") - (tr "shortcuts.delete") - (tr "shortcuts.delete-node") - (tr "shortcuts.detach-component") - (tr "shortcuts.draw-curve") - (tr "shortcuts.draw-ellipse") - (tr "shortcuts.draw-frame") - (tr "shortcuts.draw-nodes") - (tr "shortcuts.draw-path") - (tr "shortcuts.draw-rect") - (tr "shortcuts.draw-text") - (tr "shortcuts.duplicate") - (tr "shortcuts.escape") - (tr "shortcuts.export-shapes") - (tr "shortcuts.find") - (tr "shortcuts.find-and-replace") - (tr "shortcuts.fit-all") - (tr "shortcuts.flip-horizontal") - (tr "shortcuts.flip-vertical") - (tr "shortcuts.font-size-dec") - (tr "shortcuts.font-size-inc") - (tr "shortcuts.go-to-drafts") - (tr "shortcuts.go-to-libs") - (tr "shortcuts.go-to-search") - (tr "shortcuts.group") - (tr "shortcuts.h-distribute") - (tr "shortcuts.hide-ui") - (tr "shortcuts.increase-zoom") - (tr "shortcuts.insert-image") - (tr "shortcuts.italic") - (tr "shortcuts.join-nodes") - (tr "shortcuts.line-through") - (tr "shortcuts.make-corner") - (tr "shortcuts.make-curve") - (tr "shortcuts.mask") - (tr "shortcuts.merge-nodes") - (tr "shortcuts.move") - (tr "shortcuts.move-fast-down") - (tr "shortcuts.move-fast-left") - (tr "shortcuts.move-fast-right") - (tr "shortcuts.move-fast-up") - (tr "shortcuts.move-nodes") - (tr "shortcuts.move-unit-down") - (tr "shortcuts.move-unit-left") - (tr "shortcuts.move-unit-right") - (tr "shortcuts.move-unit-up") - (tr "shortcuts.next-frame") - (tr "shortcuts.opacity-0") - (tr "shortcuts.opacity-1") - (tr "shortcuts.opacity-2") - (tr "shortcuts.opacity-3") - (tr "shortcuts.opacity-4") - (tr "shortcuts.opacity-5") - (tr "shortcuts.opacity-6") - (tr "shortcuts.opacity-7") - (tr "shortcuts.opacity-8") - (tr "shortcuts.opacity-9") - (tr "shortcuts.open-color-picker") - (tr "shortcuts.open-comments") - (tr "shortcuts.open-dashboard") - (tr "shortcuts.open-inspect") - (tr "shortcuts.open-interactions") - (tr "shortcuts.open-viewer") - (tr "shortcuts.open-workspace") - (tr "shortcuts.paste") - (tr "shortcuts.paste-replace") - (tr "shortcuts.prev-frame") - (tr "shortcuts.redo") - (tr "shortcuts.rename") - (tr "shortcuts.reset-zoom") - (tr "shortcuts.scale") - (tr "shortcuts.search-placeholder") - (tr "shortcuts.select-all") - (tr "shortcuts.select-next") - (tr "shortcuts.select-parent-layer") - (tr "shortcuts.select-prev") - (tr "shortcuts.separate-nodes") - (tr "shortcuts.show-pixel-grid") - (tr "shortcuts.show-shortcuts") - (tr "shortcuts.snap-nodes") - (tr "shortcuts.snap-pixel-grid") - (tr "shortcuts.start-editing") - (tr "shortcuts.start-measure") - (tr "shortcuts.stop-measure") - (tr "shortcuts.thumbnail-set") - (tr "shortcuts.toggle-alignment") - (tr "shortcuts.toggle-assets") - (tr "shortcuts.toggle-colorpalette") - (tr "shortcuts.toggle-focus-mode") - (tr "shortcuts.toggle-fullscreen") - (tr "shortcuts.toggle-guides") - (tr "shortcuts.toggle-history") - (tr "shortcuts.toggle-layers") - (tr "shortcuts.toggle-layout-flex") - (tr "shortcuts.toggle-layout-grid") - (tr "shortcuts.toggle-lock") - (tr "shortcuts.toggle-lock-size") - (tr "shortcuts.toggle-rulers") - (tr "shortcuts.toggle-snap-guides") - (tr "shortcuts.toggle-snap-ruler-guide") - (tr "shortcuts.toggle-textpalette") - (tr "shortcuts.toggle-theme") - (tr "shortcuts.toggle-visibility") - (tr "shortcuts.toggle-zoom-style") - (tr "shortcuts.underline") - (tr "shortcuts.undo") - (tr "shortcuts.ungroup") - (tr "shortcuts.unmask") - (tr "shortcuts.v-distribute") - (tr "shortcuts.zoom-lense-decrease") - (tr "shortcuts.zoom-lense-increase") - (tr "shortcuts.zoom-selected")) - - (let [translat-pre (case type - :sc "shortcuts." - :sec "shortcut-section." - :sub-sec "shortcut-subsection.")] - (tr (str translat-pre (d/name keyname))))) - -(defn add-translation - [type item] - (map (fn [[k v]] [k (assoc v :translation (translation-keyname type k))]) item)) - -(defn shortcuts->subsections - "A function to obtain the list of subsections and their - associated shortcus from the general map of shortcuts" - [shortcuts] - (let [subsections (into #{} (mapcat :subsections) (vals shortcuts)) - get-sc-by-subsection - (fn [subsection [k v]] - (when (some #(= subsection %) (:subsections v)) {k v})) - reduce-sc - (fn [acc subsection] - (let [shortcuts-by-subsection (into {} (keep (partial get-sc-by-subsection subsection) shortcuts))] - (assoc acc subsection {:children shortcuts-by-subsection})))] - (reduce reduce-sc {} subsections))) - -(mf/defc shortcuts-keys* - [{:keys [content command]}] - (let [managed-list (if (coll? content) - content - (conj () content)) - chars-list (map ds/split-sc managed-list) - last-element (last chars-list) - short-char-list (if (= 1 (count chars-list)) - chars-list - (drop-last chars-list)) - penultimate (last short-char-list)] - [:span {:class (stl/css :keys)} - (for [chars short-char-list] - [:* {:key (str/join chars)} - (for [char chars] - [:> converted-chars* {:key (dm/str char "-" (name command)) - :char char - :command command}]) - (when (not= chars penultimate) [:span {:class (stl/css :space)} ","])]) - (when (not= last-element penultimate) - [:* - [:span {:class (stl/css :space)} (tr "shortcuts.or")] - (for [char last-element] - [:> converted-chars* {:key (dm/str char "-" (name command)) - :char char - :command command}])])])) - -(mf/defc shortcut-row* - [{:keys [elements filter-term is-match-section is-match-subsection - editable? custom-shortcuts on-edit on-reset]}] - (let [shortcut-name (keys elements) - shortcut-translations (map #(translation-keyname :sc %) shortcut-name) - match-shortcut? (some #(matches-search % @filter-term) shortcut-translations) - filtered (if (and (or is-match-section is-match-subsection) (not match-shortcut?)) - shortcut-translations - (filter #(matches-search % @filter-term) shortcut-translations)) - sorted-filtered (sort filtered)] - - [:ul {:class (stl/css :sub-menu)} - (for [command-translate sorted-filtered] - (let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements)) - [command comand-info] sc-by-translate - content (or (:show-command comand-info) (:command comand-info)) - customized? (and editable? (contains? custom-shortcuts command))] - [:li {:class (stl/css-case :shortcuts-name true - :customized customized?) - :key command-translate} - [:span {:class (stl/css :command-name)} - command-translate] - [:div {:class (stl/css :shortcut-actions)} - [:> shortcuts-keys* {:content content - :command command}] - (when editable? - [:div {:class (stl/css :edit-buttons)} - (when customized? - [:> icon-button* {:variant "ghost" - :aria-label (tr "shortcuts.reset") - :on-click (fn [e] - (dom/stop-propagation e) - (on-reset command)) - :icon i/reload - :icon-size "s"}]) - [:> icon-button* {:variant "ghost" - :aria-label (tr "shortcuts.edit") - :on-click (fn [e] - (dom/stop-propagation e) - (on-edit command)) - :icon i/curve - :icon-size "s"}]])]]))])) - -(mf/defc section-title* - [{:keys [name is-visible is-sub]}] - [:div {:class (if is-sub - (stl/css :subsection-title) - (stl/css :section-title))} - [:> icon* {:icon-id (if is-visible i/arrow-down i/arrow-right) - :size "s"}] - [:span {:class (if is-sub - (stl/css :subsection-name) - (stl/css :section-name))} name]]) - -(mf/defc shortcut-subsection* - [{:keys [subsections manage-sections filter-term is-match-section open-sections - editable? custom-shortcuts on-edit on-reset]}] - (let [subsections-names (keys subsections) - subsection-translations (if (= :none (first subsections-names)) - (map #(translation-keyname :sc %) subsections-names) - (map #(translation-keyname :sub-sec %) subsections-names)) - sorted-translations (sort subsection-translations)] - ;; Basics section is treated different because it has no sub sections - (if (= :none (first subsections-names)) - (let [basic-shortcuts (:none subsections)] - [:> shortcut-row* {:elements (:children basic-shortcuts) - :filter-term filter-term - :is-match-section is-match-section - :is-match-subsection true - :editable? editable? - :custom-shortcuts custom-shortcuts - :on-edit on-edit - :on-reset on-reset}]) - - [:ul {:class (stl/css :subsection-menu)} - (for [sub-translated sorted-translations] - (let [sub-by-translate (first (filter #(= (:translation (second %)) sub-translated) subsections)) - [sub-name sub-info] sub-by-translate - visible? (some #(= % (:id sub-info)) @open-sections) - match-subsection? (matches-search (translation-keyname :sub-sec sub-name) @filter-term) - shortcut-names (map #(translation-keyname :sc %) (keys (:children sub-info))) - match-shortcuts? (some #(matches-search % @filter-term) shortcut-names)] - (when (or match-subsection? match-shortcuts? is-match-section) - [:li {:key sub-translated - :on-click (manage-sections (:id sub-info))} - [:> section-title* {:name sub-translated - :is-visible visible? - :is-sub true}] - - [:div {:style {:display (if visible? "initial" "none")}} - [:> shortcut-row* {:elements (:children sub-info) - :filter-term filter-term - :is-match-section is-match-section - :is-match-subsection match-subsection? - :editable? editable? - :custom-shortcuts custom-shortcuts - :on-edit on-edit - :on-reset on-reset}]]])))]))) - -(mf/defc shortcut-section* - [{:keys [section manage-sections open-sections filter-term - editable? custom-shortcuts on-edit on-reset]}] - (let [[section-key section-info] section - section-id (:id section-info) - section-translation (translation-keyname :sec section-key) - match-section? (matches-search section-translation @filter-term) - subsections (:children section-info) - subs-names (keys subsections) - subs-bodys (reduce #(conj %1 (:children (%2 subsections))) {} subs-names) - sub-trans (map #(if (= "none" (d/name %)) - nil - (translation-keyname :sub-sec %)) subs-names) - match-subsection? (some #(matches-search % @filter-term) sub-trans) - translations (map #(translation-keyname :sc %) (keys subs-bodys)) - match-shortcut? (some #(matches-search % @filter-term) translations) - visible? (some #(= % section-id) @open-sections)] - - (when (or match-section? match-subsection? match-shortcut?) - [:div {:class (stl/css :section) - :on-click (manage-sections section-id)} - [:> section-title* {:name section-translation - :is-visible visible? - :is-sub false}] - - [:div {:style {:display (if visible? "initial" "none")}} - [:> shortcut-subsection* {:subsections subsections - :open-sections open-sections - :manage-sections manage-sections - :is-match-section match-section? - :filter-term filter-term - :editable? editable? - :custom-shortcuts custom-shortcuts - :on-edit on-edit - :on-reset on-reset}]]]))) - (def ^:private workspace-shortcuts-raw (d/deep-merge app.main.data.workspace.path.shortcuts/shortcuts app.main.data.workspace.shortcuts/shortcuts)) @@ -413,71 +43,23 @@ workspace-shortcuts-custom (ds/apply-custom-overrides workspace-shortcuts-raw custom-shortcuts) all-workspace-shortcuts (->> workspace-shortcuts-custom - (add-translation :sc) + (ss/add-translation :sc) (into {})) dashboard-shortcuts (->> app.main.data.dashboard.shortcuts/shortcuts - (add-translation :sc) + (ss/add-translation :sc) (into {})) viewer-shortcuts (->> app.main.data.viewer.shortcuts/shortcuts - (add-translation :sc) + (ss/add-translation :sc) (into {})) open-sections (mf/use-state [[1]]) filter-term (mf/use-state "") close-fn #(st/emit! (dw/toggle-layout-flag :shortcuts)) - walk (fn walk [element parent-id] - (if (nil? element) - element - (let [rec-fn (fn [index [k item]] - (let [item-id (if (nil? parent-id) - [index] - (conj parent-id index))] - [k (assoc item :id item-id :children (walk (:children item) item-id))]))] - (into {} (map-indexed (partial rec-fn) element))))) + {:keys [all-shortcuts all-sc-names all-sub-names all-section-names]} + (ss/build-all-shortcuts all-workspace-shortcuts dashboard-shortcuts viewer-shortcuts) - workspace-sc-by-subsections (->> (shortcuts->subsections all-workspace-shortcuts) - (add-translation :sub-sec) - (into {})) - dashboard-sc-by-subsections (->> (shortcuts->subsections dashboard-shortcuts) - (add-translation :sub-sec) - (into {})) - viewer-sc-by-subsections (->> (shortcuts->subsections viewer-shortcuts) - (add-translation :sub-sec) - (into {})) - basics-elements (into {} (concat (:children (:basics workspace-sc-by-subsections)) - (:children (:basics dashboard-sc-by-subsections)) - (:children (:basics viewer-sc-by-subsections)))) - - workspace-sc-by-subsections (dissoc workspace-sc-by-subsections :basics) - dashboard-sc-by-subsections (dissoc dashboard-sc-by-subsections :basics) - viewer-sc-by-subsections (dissoc viewer-sc-by-subsections :bassics) - - all-shortcuts {:basics {:id [1] - :children {:none {:children basics-elements}} - :translation (tr "shortcut-section.basics")} - :workspace {:id [2] - :children workspace-sc-by-subsections - :translation (tr "shortcut-section.workspace")} - :dashboard {:id [3] - :children dashboard-sc-by-subsections - :translation (tr "shortcut-section.dashboard")} - :viewer {:id [4] - :children viewer-sc-by-subsections - :translation (tr "shortcut-section.viewer")}} - all-shortcuts (walk all-shortcuts nil) - - all-sc-names (map #(translation-keyname :sc %) (concat - (keys all-workspace-shortcuts) - (keys dashboard-shortcuts) - (keys viewer-shortcuts))) - - all-sub-names (map #(translation-keyname :sub-sec %) (concat - (keys workspace-sc-by-subsections) - (keys dashboard-sc-by-subsections) - (keys viewer-sc-by-subsections))) - all-section-names (map #(translation-keyname :sec %) (keys all-shortcuts)) all-item-names (concat all-sc-names all-sub-names all-section-names) match-any? (some #(matches-search % @filter-term) all-item-names) @@ -541,7 +123,7 @@ current-command (or (get custom-shortcuts shortcut-key) default-command)] (st/emit! (modal/show :shortcut-edit {:shortcut-key shortcut-key - :shortcut-name (translation-keyname :sc shortcut-key) + :shortcut-name (ss/translation-keyname :sc shortcut-key) :current-command current-command :default-command default-command :all-shortcuts workspace-shortcuts-custom}))))) @@ -580,13 +162,13 @@ (for [section all-shortcuts] (let [[section-key _] section ws-editable? (contains? #{:basics :workspace} section-key)] - [:> shortcut-section* {:key (->> section second :id first) - :section section - :manage-sections manage-sections - :open-sections open-sections - :filter-term filter-term - :editable? ws-editable? - :custom-shortcuts custom-shortcuts - :on-edit on-edit-shortcut - :on-reset on-reset-shortcut}]))] + [:> ss/shortcut-section* {:key (name section-key) + :section section + :manage-sections manage-sections + :open-sections open-sections + :filter-term filter-term + :editable? false + :custom-shortcuts custom-shortcuts + :on-edit on-edit-shortcut + :on-reset on-reset-shortcut}]))] [:div {:class (stl/css :not-found)} (tr "shortcuts.not-found")])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss index a99f6ce668..b4e65a805a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss @@ -32,17 +32,6 @@ margin: var(--sp-s) var(--sp-s) 0 var(--sp-s); } -.section { - margin: 0; -} - -.not-found { - @include deprecated.body-small-typography; - - color: var(--empty-message-foreground-color); - margin: deprecated.$s-12; -} - .shortcuts-list { display: flex; flex-direction: column; @@ -51,105 +40,11 @@ overflow-y: auto; font-size: deprecated.$fs-12; color: var(--title-foreground-color); - - .section-title, - .subsection-title { - @include deprecated.uppercase-title-typography; - - display: flex; - align-items: center; - margin: 0; - padding: deprecated.$s-8 0; - cursor: pointer; - - .subsection-name, - .section-name { - padding-left: deprecated.$s-4; - } - - &:hover { - color: var(--title-foreground-color-hover); - } - } - - .subsection-title { - text-transform: none; - padding-left: deprecated.$s-12; - } - - .subsection-menu { - margin-bottom: deprecated.$s-4; - } - - .sub-menu { - margin-bottom: deprecated.$s-4; - - .shortcuts-name { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - min-height: deprecated.$s-32; - padding: deprecated.$s-6; - margin-bottom: deprecated.$s-4; - border-radius: deprecated.$br-8; - background-color: var(--pill-background-color); - - .command-name { - @include deprecated.body-small-typography; - - margin-left: deprecated.$s-2; - color: var(--pill-foreground-color); - flex: 1; - min-width: 0; - } - - .shortcut-actions { - display: flex; - align-items: center; - gap: deprecated.$s-2; - } - - .edit-buttons { - display: flex; - align-items: center; - opacity: 0; - transition: opacity 0.15s ease; - } - - &:hover .edit-buttons { - opacity: 1; - } - - &.customized { - background-color: var(--pill-background-color-hover, var(--pill-background-color)); - - .edit-buttons { - opacity: 1; - } - } - - .keys { - @include deprecated.flex-center; - - gap: deprecated.$s-2; - color: var(--pill-foreground-color); - - .key { - @include deprecated.body-small-typography; - @include deprecated.flex-center; - - text-transform: capitalize; - height: deprecated.$s-20; - padding: deprecated.$s-2 deprecated.$s-6; - border-radius: deprecated.$s-6; - background-color: var(--menu-shortcut-background-color); - } - - .space { - margin: 0 deprecated.$s-2; - } - } - } - } +} + +.not-found { + @include deprecated.body-small-typography; + + color: var(--empty-message-foreground-color); + margin: deprecated.$s-12; }