diff --git a/.continue/mcpServers/new-mcp-server.yaml b/.continue/mcpServers/new-mcp-server.yaml new file mode 100644 index 0000000000..0e32aa6d45 --- /dev/null +++ b/.continue/mcpServers/new-mcp-server.yaml @@ -0,0 +1,10 @@ +name: New MCP server +version: 0.0.1 +schema: v1 +mcpServers: + - name: New MCP server + command: npx + args: + - -y + - + env: {} diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 0a0dc0b9c9..defc643443 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -25,6 +25,7 @@ [app.common.types.variant :as ctv] [app.common.uuid :as uuid] [app.config :as cf] + [app.main.broadcast :as mbc] [app.main.data.changes :as dch] [app.main.data.comments :as dcmt] [app.main.data.common :as dcm] @@ -214,7 +215,7 @@ (watch [_ _ _] (rx/of (dp/check-open-plugin) (fdf/fix-deleted-fonts-for-local-library file-id) - (mcp/init-mcp-connexion))))) + (mcp/init-mcp-connection))))) (defn- bundle-fetched [{:keys [file file-id thumbnails] :as bundle}] @@ -370,6 +371,12 @@ (rx/take 1) (rx/tap (fn [_] (perf/setup))))) + (when (contains? cf/flags :mcp) + (->> mbc/stream + (rx/filter (mbc/type? :mcp-enabled-change)) + (rx/map deref) + (rx/map mcp/update-mcp-status))) + (->> stream (rx/filter (ptk/type? ::dps/persistence-notification)) (rx/take 1) diff --git a/frontend/src/app/main/data/workspace/mcp.cljs b/frontend/src/app/main/data/workspace/mcp.cljs index fb86ab5c11..72544ad7cc 100644 --- a/frontend/src/app/main/data/workspace/mcp.cljs +++ b/frontend/src/app/main/data/workspace/mcp.cljs @@ -34,6 +34,27 @@ [event] (= (ptk/type event) :app.main.data.workspace/finalize-workspace)) +(defn update-mcp-status + [value] + (ptk/reify ::update-mcp-status + ptk/UpdateEvent + (update [_ state] + (update-in state [:profile :props] assoc :mcp-enabled value)) + + ptk/WatchEvent + (watch [_ _ _] + (case value + true (rx/of (ptk/data-event ::connect)) + false (rx/of (ptk/data-event ::disconnect)) + nil)))) + +(defn update-mcp-connection + [value] + (ptk/reify ::update-mcp-plugin-connection + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-local :mcp] assoc :connected value)))) + (defn init-mcp! [stream] (->> (rp/cmd! :get-current-mcp-token) @@ -52,8 +73,12 @@ :getServerUrl #(str cf/mcp-ws-uri) :setMcpStatus (fn [status] - ;; TODO: Visual feedback - (log/info :hint "MCP STATUS" :status status)) + (let [mcp-connected? (case status + "connected" true + "disconnected" false + nil)] + (st/emit! (update-mcp-connection mcp-connected?)) + (log/info :hint "MCP STATUS" :status status))) :on (fn [event cb] @@ -77,11 +102,11 @@ [] (st/emit! (ptk/data-event ::connect))) -(defn init-mcp-connexion +(defn init-mcp-connection [] - (ptk/reify ::init-mcp-connexion + (ptk/reify ::init-mcp-connection ptk/EffectEvent (effect [_ state stream] (when (and (contains? cf/flags :mcp) - (-> state :profile :props :mcp-status)) + (-> state :profile :props :mcp-enabled)) (init-mcp! stream))))) diff --git a/frontend/src/app/main/ui/settings/integrations.cljs b/frontend/src/app/main/ui/settings/integrations.cljs index 5e56b5096f..d84eef10fc 100644 --- a/frontend/src/app/main/ui/settings/integrations.cljs +++ b/frontend/src/app/main/ui/settings/integrations.cljs @@ -12,6 +12,7 @@ [app.common.schema :as sm] [app.common.time :as ct] [app.config :as cf] + [app.main.broadcast :as mbc] [app.main.data.event :as ev] [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] @@ -272,12 +273,13 @@ on-created (mf/use-fn (fn [] - (st/emit! (du/update-profile-props {:mcp-status true}) + (st/emit! (du/update-profile-props {:mcp-enabled true}) (ev/event {::ev/name "generate-mcp-key" ::ev/origin "integrations"}) (ev/event {::ev/name "enable-mcp" ::ev/origin "integrations" :source "key-creation"})) + (mbc/emit! :mcp-enabled-change true) (reset! created? true)))] [:div {:class (stl/css :modal-overlay)} @@ -315,9 +317,10 @@ (mf/use-fn (fn [] (st/emit! (du/delete-access-token {:id mcp-key-id}) - (du/update-profile-props {:mcp-status true}) + (du/update-profile-props {:mcp-enabled true}) (ev/event {::ev/name "regenerate-mcp-key" ::ev/origin "integrations"})) + (mbc/emit! :mcp-enabled-change true) (reset! created? true)))] [:div {:class (stl/css :modal-overlay)} @@ -406,8 +409,8 @@ (let [tokens (mf/deref tokens-ref) profile (mf/deref refs/profile) - mcp-key (some #(when (= (:type %) "mcp") %) tokens) - mcp-active? (d/nilv (-> profile :props :mcp-status) false) + mcp-key (some #(when (= (:type %) "mcp") %) tokens) + mcp-enabled? (d/nilv (-> profile :props :mcp-enabled) false) expires-at (:expires-at mcp-key) expired? (and (some? expires-at) (> (ct/now) expires-at)) @@ -415,21 +418,22 @@ tooltip-id (mf/use-id) - handle-mcp-status-change + handle-mcp-change (mf/use-fn - (fn [mcp-status] - (st/emit! (du/update-profile-props {:mcp-status mcp-status}) + (fn [value] + (st/emit! (du/update-profile-props {:mcp-enabled value}) (ntf/show {:level :info :type :toast - :content (if (true? mcp-status) + :content (if (true? value) (tr "integrations.notification.success.mcp-server-enabled") (tr "integrations.notification.success.mcp-server-disabled")) :timeout notification-timeout}) - (ev/event {::ev/name (if (true? mcp-status) "enable-mcp" "disable-mcp") + (ev/event {::ev/name (if (true? value) "enable-mcp" "disable-mcp") ::ev/origin "integrations" - :source "toggle"})))) + :source "toggle"})) + (mbc/emit! :mcp-enabled-change value))) - handle-initial-mcp-status + handle-generate-mcp-key (mf/use-fn #(st/emit! (modal/show {:type :generate-mcp-key}))) @@ -444,7 +448,8 @@ (let [params {:id (:id mcp-key)} mdata {:on-success #(st/emit! (du/fetch-access-tokens))}] (st/emit! (du/delete-access-token (with-meta params mdata)) - (du/update-profile-props {:mcp-status false}))))) + (du/update-profile-props {:mcp-enabled false})) + (mbc/emit! :mcp-enabled-change false)))) on-copy-to-clipboard (mf/use-fn @@ -497,14 +502,14 @@ (tr "integrations.mcp-server.status.expired.1")]]]) [:div {:class (stl/css :mcp-server-switch)} - [:> switch* {:label (if mcp-active? + [:> switch* {:label (if mcp-enabled? (tr "integrations.mcp-server.status.enabled") (tr "integrations.mcp-server.status.disabled")) - :default-checked mcp-active? - :on-change handle-mcp-status-change}] - (when (and (false? mcp-active?) (nil? mcp-key)) + :default-checked mcp-enabled? + :on-change handle-mcp-change}] + (when (and (false? mcp-enabled?) (nil? mcp-key)) [:div {:class (stl/css :mcp-server-switch-cover) - :on-click handle-initial-mcp-status}])]]] + :on-click handle-generate-mcp-key}])]]] (when (some? mcp-key) [:div {:class (stl/css :mcp-server-key)} diff --git a/frontend/src/app/main/ui/workspace/left_header.cljs b/frontend/src/app/main/ui/workspace/left_header.cljs index 08f1488d9f..09f34268a5 100644 --- a/frontend/src/app/main/ui/workspace/left_header.cljs +++ b/frontend/src/app/main/ui/workspace/left_header.cljs @@ -15,7 +15,6 @@ [app.main.refs :as refs] [app.main.router :as rt] [app.main.store :as st] - [app.main.ui.context :as ctx] [app.main.ui.icons :as deprecated-icon] [app.main.ui.workspace.main-menu :as main-menu] [app.util.dom :as dom] @@ -27,12 +26,10 @@ ;; --- Header Component (mf/defc left-header* - [{:keys [file layout project page-id class]}] - (let [profile (mf/deref refs/profile) - file-id (:id file) + [{:keys [file layout project class]}] + (let [file-id (:id file) file-name (:name file) project-id (:id project) - team-id (:team-id project) shared? (:is-shared file) persistence (mf/deref refs/persistence) @@ -40,8 +37,6 @@ persistence-status (get persistence :status) - read-only? (mf/use-ctx ctx/workspace-read-only?) - editing* (mf/use-state false) editing? (deref editing*) input-ref (mf/use-ref nil) @@ -137,10 +132,5 @@ (when ^boolean shared? [:span {:class (stl/css :shared-badge)} deprecated-icon/library]) [:div {:class (stl/css :menu-section)} - [:& main-menu/menu - {:layout layout - :file file - :profile profile - :read-only? read-only? - :team-id team-id - :page-id page-id}]]])) + [:> main-menu/menu* {:layout layout + :file file}]]])) diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index a964d27475..aa320694c7 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -22,6 +22,7 @@ [app.main.data.shortcuts :as scd] [app.main.data.workspace :as dw] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.mcp :as mcp] [app.main.data.workspace.shortcuts :as sc] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.versions :as dwv] @@ -34,9 +35,8 @@ [app.main.ui.dashboard.subscription :refer [get-subscription-type main-menu-power-up*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] - [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.hooks.resize :as r] - [app.main.ui.icons :as deprecated-icon] [app.plugins.register :as preg] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -45,11 +45,17 @@ [potok.v2.core :as ptk] [rumext.v2 :as mf])) -;; --- Header menu and submenus +(mf/defc shortcuts* + {::mf/private true} + [{:keys [id]}] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip id))] + [:span {:class (stl/css :shortcut-key) + :key sc} + sc])]) (mf/defc help-info-menu* - {::mf/props :obj - ::mf/private true + {::mf/private true ::mf/wrap [mf/memo]} [{:keys [layout on-close]}] (let [nav-to-helpc-center @@ -100,6 +106,9 @@ plugins? (features/active-feature? @st/state "plugins/runtime") + mcp? + (contains? cf/flags :mcp) + show-shortcuts (mf/use-fn (mf/deps layout) @@ -115,213 +124,206 @@ (mf/use-fn (fn [event] (let [version (:main cf/version)] - (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version})) + (st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" + :version version})) (println version) (if (and (kbd/alt? event) (kbd/mod? event)) (st/emit! (modal/show {:type :onboarding})) - (st/emit! (modal/show {:type :release-notes :version version}))))))] + (st/emit! (modal/show {:type :release-notes + :version version}))))))] [:> dropdown-menu* {:show true - ;; :id "workspace-help-menu" :on-close on-close - :class (stl/css-case :sub-menu true - :help-info plugins? - :help-info-old (not plugins?))} - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + :class (stl/css-case :base-menu true + :sub-menu true + :pos-final-5 (not (or plugins? mcp?)) + :pos-final-6 (not= plugins? mcp?) + :pos-final-7 (and plugins? mcp?))} + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click nav-to-helpc-center :on-key-down (fn [event] (when (kbd/enter? event) (nav-to-helpc-center event))) :id "file-menu-help-center"} - [:span {:class (stl/css :item-name)} (tr "labels.help-center")]] + [:span {:class (stl/css :item-name)} + (tr "labels.help-center")]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click nav-to-community :on-key-down (fn [event] (when (kbd/enter? event) (nav-to-community event))) :id "file-menu-community"} - [:span {:class (stl/css :item-name)} (tr "labels.community")]] + [:span {:class (stl/css :item-name)} + (tr "labels.community")]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click nav-to-youtube :on-key-down (fn [event] (when (kbd/enter? event) (nav-to-youtube event))) :id "file-menu-youtube"} - [:span {:class (stl/css :item-name)} (tr "labels.tutorials")]] + [:span {:class (stl/css :item-name)} + (tr "labels.tutorials")]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click show-release-notes :on-key-down (fn [event] (when (kbd/enter? event) (show-release-notes event))) :id "file-menu-release-notes"} - [:span {:class (stl/css :item-name)} (tr "labels.release-notes")]] + [:span {:class (stl/css :item-name)} + (tr "labels.release-notes")]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click nav-to-templates :on-key-down (fn [event] (when (kbd/enter? event) (nav-to-templates event))) :id "file-menu-templates"} - [:span {:class (stl/css :item-name)} (tr "labels.libraries-and-templates")]] + [:span {:class (stl/css :item-name)} + (tr "labels.libraries-and-templates")]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click nav-to-github :on-key-down (fn [event] (when (kbd/enter? event) (nav-to-github event))) :id "file-menu-github"} - [:span {:class (stl/css :item-name)} (tr "labels.github-repo")]] + [:span {:class (stl/css :item-name)} + (tr "labels.github-repo")]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click nav-to-terms :on-key-down (fn [event] (when (kbd/enter? event) (nav-to-terms event))) :id "file-menu-terms"} - [:span {:class (stl/css :item-name)} (tr "auth.terms-of-service")]] + [:span {:class (stl/css :item-name)} + (tr "auth.terms-of-service")]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click show-shortcuts :on-key-down (fn [event] (when (kbd/enter? event) (show-shortcuts event))) :id "file-menu-shortcuts"} - [:span {:class (stl/css :item-name)} (tr "label.shortcuts")] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :show-shortcuts))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:span {:class (stl/css :item-name)} + (tr "label.shortcuts")] + [:> shortcuts* {:id :show-shortcuts}]] (when (contains? cf/flags :user-feedback) - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click nav-to-feedback :on-key-down (fn [event] (when (kbd/enter? event) (nav-to-feedback event))) :id "file-menu-feedback"} - [:span {:class (stl/css-case :feedback true - :item-name true)} (tr "labels.give-feedback")]])])) + [:span {:class (stl/css :feedback :item-name)} + (tr "labels.give-feedback")]])])) (mf/defc preferences-menu* - {::mf/props :obj - ::mf/private true + {::mf/private true ::mf/wrap [mf/memo]} [{:keys [layout profile toggle-flag on-close toggle-theme]}] - (let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))] + (let [show-nudge-options + (mf/use-fn + #(modal/show! {:type :nudge-option}))] [:> dropdown-menu* {:show true - ;; :id "workspace-preferences-menu" - :class (stl/css-case :sub-menu true - :preferences true) + :class (stl/css :base-menu :sub-menu :pos-4) :on-close on-close} [:> dropdown-menu-item* {:on-click toggle-flag - :class (stl/css :submenu-item) + :class (stl/css :base-menu-item :submenu-item) :on-key-down (fn [event] (when (kbd/enter? event) (toggle-flag event))) - :data-testid "scale-text" + :data-testid "scale-text" :id "file-menu-scale-text"} [:span {:class (stl/css :item-name)} (if (contains? layout :scale-text) (tr "workspace.header.menu.disable-scale-content") (tr "workspace.header.menu.enable-scale-content"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :scale))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :scale}]] [:> dropdown-menu-item* {:on-click toggle-flag - :class (stl/css :submenu-item) + :class (stl/css :base-menu-item :submenu-item) :on-key-down (fn [event] (when (kbd/enter? event) (toggle-flag event))) - :data-testid "snap-ruler-guides" + :data-testid "snap-ruler-guides" :id "file-menu-snap-ruler-guides"} [:span {:class (stl/css :item-name)} (if (contains? layout :snap-ruler-guides) (tr "workspace.header.menu.disable-snap-ruler-guides") (tr "workspace.header.menu.enable-snap-ruler-guides"))] - [:span {:class (stl/css :shortcut)} - - (for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-ruler-guide))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :toggle-snap-ruler-guide}]] [:> dropdown-menu-item* {:on-click toggle-flag - :class (stl/css :submenu-item) + :class (stl/css :base-menu-item :submenu-item) :on-key-down (fn [event] (when (kbd/enter? event) (toggle-flag event))) - :data-testid "snap-guides" + :data-testid "snap-guides" :id "file-menu-snap-guides"} [:span {:class (stl/css :item-name)} (if (contains? layout :snap-guides) (tr "workspace.header.menu.disable-snap-guides") (tr "workspace.header.menu.enable-snap-guides"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-snap-guides))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :toggle-snap-guides}]] [:> dropdown-menu-item* {:on-click toggle-flag - :class (stl/css :submenu-item) + :class (stl/css :base-menu-item :submenu-item) :on-key-down (fn [event] (when (kbd/enter? event) (toggle-flag event))) - :data-testid "dynamic-alignment" + :data-testid "dynamic-alignment" :id "file-menu-dynamic-alignment"} [:span {:class (stl/css :item-name)} (if (contains? layout :dynamic-alignment) (tr "workspace.header.menu.disable-dynamic-alignment") (tr "workspace.header.menu.enable-dynamic-alignment"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-alignment))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :toggle-alignment}]] [:> dropdown-menu-item* {:on-click toggle-flag - :class (stl/css :submenu-item) + :class (stl/css :base-menu-item :submenu-item) :on-key-down (fn [event] (when (kbd/enter? event) (toggle-flag event))) - :data-testid "snap-pixel-grid" + :data-testid "snap-pixel-grid" :id "file-menu-pixel-grid"} [:span {:class (stl/css :item-name)} (if (contains? layout :snap-pixel-grid) (tr "workspace.header.menu.disable-snap-pixel-grid") (tr "workspace.header.menu.enable-snap-pixel-grid"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :snap-pixel-grid))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :snap-pixel-grid}]] [:> dropdown-menu-item* {:on-click show-nudge-options - :class (stl/css :submenu-item) + :class (stl/css :base-menu-item :submenu-item) :on-key-down (fn [event] (when (kbd/enter? event) (show-nudge-options event))) - :data-testid "snap-pixel-grid" + :data-testid "snap-pixel-grid" :id "file-menu-nudge"} [:span {:class (stl/css :item-name)} (tr "modals.nudge-title")]] - [:> dropdown-menu-item* {:on-click toggle-theme - :class (stl/css :submenu-item) + :class (stl/css :base-menu-item :submenu-item) :on-key-down (fn [event] (when (kbd/enter? event) (toggle-theme event))) - :data-testid "toggle-theme" + :data-testid "toggle-theme" :id "file-menu-toggle-theme"} [:span {:class (stl/css :item-name)} (case (:theme profile) ;; dark -> light -> system -> dark and so on "dark" (tr "workspace.header.menu.toggle-light-theme") - "light" (tr "workspace.header.menu.toggle-system-theme") + "light" (tr "workspace.header.menu.toggle-system-theme") "system" (tr "workspace.header.menu.toggle-dark-theme") (tr "workspace.header.menu.toggle-light-theme"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-theme))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]]])) + [:> shortcuts* {:id :toggle-theme}]]])) (mf/defc view-menu* - {::mf/props :obj - ::mf/private true + {::mf/private true ::mf/wrap [mf/memo]} [{:keys [layout toggle-flag on-close]}] (let [read-only? (mf/use-ctx ctx/workspace-read-only?) @@ -343,46 +345,40 @@ (vary-meta assoc ::ev/origin "workspace-menu")))))] [:> dropdown-menu* {:show true - ;; :id "workspace-view-menu" - :class (stl/css-case :sub-menu true - :view true) + :class (stl/css :base-menu :sub-menu :pos-3) :on-close on-close} - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click toggle-flag :on-key-down (fn [event] (when (kbd/enter? event) (toggle-flag event))) - :data-testid "rulers" + :data-testid "rulers" :id "file-menu-rulers"} [:span {:class (stl/css :item-name)} (if (contains? layout :rulers) (tr "workspace.header.menu.hide-rules") (tr "workspace.header.menu.show-rules"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-rulers))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :toggle-rulers}]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click toggle-flag :on-key-down (fn [event] (when (kbd/enter? event) (toggle-flag event))) - :data-testid "display-guides" + :data-testid "display-guides" :id "file-menu-guides"} [:span {:class (stl/css :item-name)} (if (contains? layout :display-guides) (tr "workspace.header.menu.hide-guides") (tr "workspace.header.menu.show-guides"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-guides))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :toggle-guides}]] (when-not ^boolean read-only? [:* - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click toggle-color-palette :on-key-down (fn [event] (when (kbd/enter? event) @@ -392,11 +388,9 @@ (if (contains? layout :colorpalette) (tr "workspace.header.menu.hide-palette") (tr "workspace.header.menu.show-palette"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-colorpalette))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :toggle-colorpalette}]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click toggle-text-palette :on-key-down (fn [event] (when (kbd/enter? event) @@ -406,68 +400,68 @@ (if (contains? layout :textpalette) (tr "workspace.header.menu.hide-textpalette") (tr "workspace.header.menu.show-textpalette"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-textpalette))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]]]) + [:> shortcuts* {:id :toggle-textpalette}]]]) - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click toggle-flag :on-key-down (fn [event] (when (kbd/enter? event) (toggle-flag event))) - :data-testid "display-artboard-names" + :data-testid "display-artboard-names" :id "file-menu-artboards"} [:span {:class (stl/css :item-name)} (if (contains? layout :display-artboard-names) (tr "workspace.header.menu.hide-artboard-names") (tr "workspace.header.menu.show-artboard-names"))]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click toggle-flag :on-key-down (fn [event] (when (kbd/enter? event) (toggle-flag event))) - :data-testid "show-pixel-grid" + :data-testid "show-pixel-grid" :id "file-menu-pixel-grid"} [:span {:class (stl/css :item-name)} (if (contains? layout :show-pixel-grid) (tr "workspace.header.menu.hide-pixel-grid") (tr "workspace.header.menu.show-pixel-grid"))] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :show-pixel-grid))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :show-pixel-grid}]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click toggle-flag :on-key-down (fn [event] (when (kbd/enter? event) (toggle-flag event))) - :data-testid "hide-ui" + :data-testid "hide-ui" :id "file-menu-hide-ui"} [:span {:class (stl/css :item-name)} (tr "workspace.shape.menu.hide-ui")] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :hide-ui))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]]])) + [:> shortcuts* {:id :hide-ui}]]])) (mf/defc edit-menu* - {::mf/props :obj - ::mf/private true + {::mf/private true ::mf/wrap [mf/memo]} [{:keys [on-close]}] - (let [select-all (mf/use-fn #(st/emit! (dw/select-all))) - undo (mf/use-fn #(st/emit! dwu/undo)) - redo (mf/use-fn #(st/emit! dwu/redo)) - perms (mf/use-ctx ctx/permissions) - can-edit (:can-edit perms)] + (let [perms (mf/use-ctx ctx/permissions) + can-edit (:can-edit perms) + + select-all + (mf/use-fn + #(st/emit! (dw/select-all))) + + undo + (mf/use-fn + #(st/emit! dwu/undo)) + + redo + (mf/use-fn + #(st/emit! dwu/redo))] [:> dropdown-menu* {:show true - ;; :id "workspace-edit-menu" - :class (stl/css-case :sub-menu true - :edit true) + :class (stl/css :base-menu :sub-menu :pos-2) :on-close on-close} - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click select-all :on-key-down (fn [event] (when (kbd/enter? event) @@ -475,45 +469,32 @@ :id "file-menu-select-all"} [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.select-all")] - [:span {:class (stl/css :shortcut)} - - (for [sc (scd/split-sc (sc/get-tooltip :select-all))] - [:span {:class (stl/css :shortcut-key) - :key sc} - sc])]] + [:> shortcuts* {:id :select-all}]] (when can-edit - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click undo :on-key-down (fn [event] (when (kbd/enter? event) (undo event))) :id "file-menu-undo"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.undo")] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :undo))] - [:span {:class (stl/css :shortcut-key) - :key sc} - sc])]]) + [:span {:class (stl/css :item-name)} + (tr "workspace.header.menu.undo")] + [:> shortcuts* {:id :undo}]]) (when can-edit - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click redo :on-key-down (fn [event] (when (kbd/enter? event) (redo event))) :id "file-menu-redo"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.redo")] - [:span {:class (stl/css :shortcut)} - - (for [sc (scd/split-sc (sc/get-tooltip :redo))] - [:span {:class (stl/css :shortcut-key) - :key sc} - sc])]])])) + [:span {:class (stl/css :item-name)} + (tr "workspace.header.menu.redo")] + [:> shortcuts* {:id :redo}]])])) (mf/defc file-menu* - {::mf/props :obj - ::mf/private true} + {::mf/private true} [{:keys [on-close file]}] (let [file-id (:id file) shared? (:is-shared file) @@ -536,12 +517,11 @@ (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (modal/show! - {:type :delete-shared-libraries - :origin :unpublish - :ids #{file-id} - :on-accept #(st/emit! (dwl/set-file-shared file-id false)) - :count-libraries 1}))) + (modal/show! {:type :delete-shared-libraries + :origin :unpublish + :ids #{file-id} + :on-accept #(st/emit! (dwl/set-file-shared file-id false)) + :count-libraries 1}))) on-remove-shared-key-down (mf/use-fn @@ -590,7 +570,8 @@ (on-pin-version event)))) on-export-shapes - (mf/use-fn #(st/emit! (de/show-workspace-export-dialog {:origin "workspace:menu"}))) + (mf/use-fn + #(st/emit! (de/show-workspace-export-dialog {:origin "workspace:menu"}))) on-export-shapes-key-down (mf/use-fn @@ -627,14 +608,12 @@ (on-export-frames event))))] [:> dropdown-menu* {:show true - ;; :id "workspace-file-menu" - :class (stl/css-case :sub-menu true - :file true) + :class (stl/css :base-menu :sub-menu :pos-1) :on-close on-close} (if ^boolean shared? (when can-edit - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click on-remove-shared :on-key-down on-remove-shared-key-down :id "file-menu-remove-shared"} @@ -642,7 +621,7 @@ (tr "dashboard.unpublish-shared")]]) (when can-edit - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click on-add-shared :on-key-down on-add-shared-key-down :id "file-menu-add-shared"} @@ -653,35 +632,32 @@ [:* [:div {:class (stl/css :separator)}] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click on-pin-version :on-key-down on-pin-version-key-down :id "file-menu-create-version"} [:span {:class (stl/css :item-name)} (tr "dashboard.create-version-menu")]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click on-show-version-history :on-key-down on-show-version-history-key-down :id "file-menu-show-version-history"} [:span {:class (stl/css :item-name)} (tr "dashboard.show-version-history")] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :toggle-history))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :toggle-history}]] [:div {:class (stl/css :separator)}]]) - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click on-export-shapes :on-key-down on-export-shapes-key-down :id "file-menu-export-shapes"} - [:span {:class (stl/css :item-name)} (tr "dashboard.export-shapes")] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :export-shapes))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:span {:class (stl/css :item-name)} + (tr "dashboard.export-shapes")] + [:> shortcuts* {:id :export-shapes}]] - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click on-export-file :on-key-down on-export-file-key-down :data-format "binfile-v3" @@ -690,7 +666,7 @@ (tr "dashboard.download-binary-file")]] (when (seq frames) - [:> dropdown-menu-item* {:class (stl/css :submenu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item) :on-click on-export-frames :on-key-down on-export-frames-key-down :id "file-menu-export-frames"} @@ -698,30 +674,26 @@ (tr "dashboard.export-frames")]])])) (mf/defc plugins-menu* - {::mf/props :obj - ::mf/private true + {::mf/private true ::mf/wrap [mf/memo]} [{:keys [open-plugins on-close]}] (when (features/active-feature? @st/state "plugins/runtime") - (let [plugins (preg/plugins-list) - user-can-edit? (:can-edit (deref refs/permissions)) - permissions-peek (deref refs/plugins-permissions-peek)] + (let [plugins (preg/plugins-list) + user-can-edit? (:can-edit (deref refs/permissions)) + permissions-peek (deref refs/plugins-permissions-peek)] [:> dropdown-menu* {:show true - ;; :id "workspace-plugins-menu" - :class (stl/css-case :sub-menu true :plugins true) + :class (stl/css :base-menu :sub-menu :pos-5 :plugins) :on-close on-close} [:> dropdown-menu-item* {:on-click open-plugins - :class (stl/css :submenu-item) + :class (stl/css :base-menu-item :submenu-item) :on-key-down (fn [event] (when (kbd/enter? event) (open-plugins event))) - :data-testid "open-plugins" + :data-testid "open-plugins" :id "file-menu-open-plugins"} [:span {:class (stl/css :item-name)} (tr "workspace.plugins.menu.plugins-manager")] - [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :plugins))] - [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + [:> shortcuts* {:id :plugins}]] (when (d/not-empty? plugins) @@ -756,28 +728,103 @@ :name name :host host})) (dp/open-plugin! manifest user-can-edit?)))))] + [:> dropdown-menu-item* {:key (dm/str "plugins-menu-" idx) :on-click on-click - :class (stl/css-case :submenu-item true :menu-disabled (not can-open?)) + :class (stl/css-case :base-menu-item true + :submenu-item true + :disabled (not can-open?)) :on-key-down on-key-down} [:span {:class (stl/css :item-name)} name] (when-not can-open? - [:span {:class (stl/css :item-icon) - :title (tr "workspace.plugins.error.need-editor")} deprecated-icon/help])]))]))) + [:span {:title (tr "workspace.plugins.error.need-editor")} + [:> icon* {:icon-id i/help + :class (stl/css :item-icon)}]])]))]))) -(mf/defc menu - {::mf/props :obj} - [{:keys [layout file profile]}] - (let [show-menu* (mf/use-state false) - show-menu? (deref show-menu*) - sub-menu* (mf/use-state false) - sub-menu (deref sub-menu*) +(mf/defc mcp-menu* + {::mf/private true} + [{:keys [on-close]}] + (let [plugins? (features/active-feature? @st/state "plugins/runtime") - open-menu + profile (mf/deref refs/profile) + workspace-local (mf/deref refs/workspace-local) + + mcp-enabled? (-> profile :props :mcp-enabled) + mcp-connected? (-> workspace-local :mcp :connected) + + on-nav-to-integrations + (mf/use-fn + (fn [] + (st/emit! (ptk/event ::ev/event {::ev/name "manage-mpc-option" + ::ev/origin "workspace-menu"})) + (dom/open-new-window "/#/settings/integrations"))) + + on-nav-to-integrations-key-down + (mf/use-fn + (fn [event] + (when (kbd/enter? event) + (on-nav-to-integrations)))) + + on-toggle-mcp-plugin + (mf/use-fn + (fn [] + (if mcp-connected? + (st/emit! (mcp/disconnect-mcp) + (ptk/event ::ev/event {::ev/name "disconnect-mcp-plugin" + ::ev/origin "workspace-menu"})) + (st/emit! (mcp/connect-mcp) + (ptk/event ::ev/event {::ev/name "connect-mcp-plugin" + ::ev/origin "workspace-menu"}))))) + + on-toggle-mcp-plugin-key-down + (mf/use-fn + (fn [event] + (when (kbd/enter? event) + (on-toggle-mcp-plugin))))] + + [:> dropdown-menu* {:show true + :class (stl/css-case :base-menu true + :sub-menu true + :pos-5 (not plugins?) + :pos-6 plugins?) + :on-close on-close} + + (when mcp-enabled? + [:> dropdown-menu-item* {:id "mcp-menu-toggle-mcp-plugin" + :class (stl/css :base-menu-item :submenu-item) + :on-click on-toggle-mcp-plugin + :on-key-down on-toggle-mcp-plugin-key-down} + [:span {:class (stl/css :item-name)} + (if mcp-connected? + (tr "workspace.header.menu.mcp.plugin.status.disconnect") + (tr "workspace.header.menu.mcp.plugin.status.connect"))]]) + + [:> dropdown-menu-item* {:id "mcp-menu-nav-to-integrations" + :class (stl/css :base-menu-item :submenu-item) + :on-click on-nav-to-integrations + :on-key-down on-nav-to-integrations-key-down} + [:span {:class (stl/css :item-name)} + (if mcp-enabled? + (tr "workspace.header.menu.mcp.server.status.enabled") + (tr "workspace.header.menu.mcp.server.status.disabled"))]]])) + +(mf/defc menu* + [{:keys [layout file]}] + (let [profile (mf/deref refs/profile) + workspace-local (mf/deref refs/workspace-local) + + show-menu* (mf/use-state false) + show-menu? (deref show-menu*) + selected-sub-menu* (mf/use-state nil) + selected-sub-menu (deref selected-sub-menu*) + + toggle-menu (mf/use-fn (fn [event] (dom/stop-propagation event) - (reset! show-menu* true))) + (swap! show-menu* not) + (when (not show-menu?) + (reset! selected-sub-menu* nil)))) close-menu (mf/use-fn @@ -789,13 +836,13 @@ (mf/use-fn (fn [event] (dom/stop-propagation event) - (reset! sub-menu* nil))) + (reset! selected-sub-menu* nil))) close-all-menus (mf/use-fn (fn [] (reset! show-menu* false) - (reset! sub-menu* nil))) + (reset! selected-sub-menu* nil))) on-menu-click (mf/use-fn @@ -804,12 +851,13 @@ (let [menu (-> (dom/get-current-target event) (dom/get-data "testid") (keyword))] - (reset! sub-menu* menu)))) + (reset! selected-sub-menu* menu)))) on-power-up-click (mf/use-fn (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "explore-pricing-click" ::ev/origin "workspace-menu"})) + (st/emit! (ptk/event ::ev/event {::ev/name "explore-pricing-click" + ::ev/origin "workspace-menu"})) (dom/open-new-window "https://penpot.app/pricing"))) toggle-flag @@ -823,7 +871,7 @@ (-> (dw/toggle-layout-flag flag) (vary-meta assoc ::ev/origin "workspace-menu"))) (reset! show-menu* false) - (reset! sub-menu* nil)))) + (reset! selected-sub-menu* nil)))) toggle-theme (mf/use-fn @@ -836,9 +884,10 @@ (fn [event] (dom/stop-propagation event) (reset! show-menu* false) - (reset! sub-menu* nil) + (reset! selected-sub-menu* nil) (st/emit! - (ptk/event ::ev/event {::ev/name "open-plugins-manager" ::ev/origin "workspace:menu"}) + (ptk/event ::ev/event {::ev/name "open-plugins-manager" + ::ev/origin "workspace:menu"}) (modal/show :plugin-management {})))) subscription (:subscription (:props profile)) @@ -853,15 +902,16 @@ [:* [:> icon-button* {:variant "ghost" + :aria-pressed show-menu? :aria-label (tr "shortcut-subsection.main-menu") - :on-click open-menu + :on-click toggle-menu :icon i/menu}] [:> dropdown-menu* {:show show-menu? :id "workspace-menu" :on-close close-menu - :class (stl/css :menu)} - [:> dropdown-menu-item* {:class (stl/css :menu-item) + :class (stl/css :base-menu :menu)} + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click :on-key-down (fn [event] (when (kbd/enter? event) @@ -869,111 +919,143 @@ :on-pointer-enter on-menu-click :data-testid "file" :id "file-menu-file"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.file")] - [:span {:class (stl/css :open-arrow)} deprecated-icon/arrow]] + [:span {:class (stl/css :item-name)} + (tr "workspace.header.menu.option.file")] + [:> icon* {:icon-id i/arrow-right + :class (stl/css :item-arrow)}]] - [:> dropdown-menu-item* {:class (stl/css :menu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click :on-key-down (fn [event] (when (kbd/enter? event) (on-menu-click event))) :on-pointer-enter on-menu-click - :data-testid "edit" + :data-testid "edit" :id "file-menu-edit"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.edit")] - [:span {:class (stl/css :open-arrow)} deprecated-icon/arrow]] + [:span {:class (stl/css :item-name)} + (tr "workspace.header.menu.option.edit")] + [:> icon* {:icon-id i/arrow-right + :class (stl/css :item-arrow)}]] - [:> dropdown-menu-item* {:class (stl/css :menu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click :on-key-down (fn [event] (when (kbd/enter? event) (on-menu-click event))) :on-pointer-enter on-menu-click - :data-testid "view" + :data-testid "view" :id "file-menu-view"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.view")] - [:span {:class (stl/css :open-arrow)} deprecated-icon/arrow]] + [:span {:class (stl/css :item-name)} + (tr "workspace.header.menu.option.view")] + [:> icon* {:icon-id i/arrow-right + :class (stl/css :item-arrow)}]] - [:> dropdown-menu-item* {:class (stl/css :menu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click :on-key-down (fn [event] (when (kbd/enter? event) (on-menu-click event))) :on-pointer-enter on-menu-click - :data-testid "preferences" + :data-testid "preferences" :id "file-menu-preferences"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.preferences")] - [:span {:class (stl/css :open-arrow)} deprecated-icon/arrow]] + [:span {:class (stl/css :item-name)} + (tr "workspace.header.menu.option.preferences")] + [:> icon* {:icon-id i/arrow-right + :class (stl/css :item-arrow)}]] (when (features/active-feature? @st/state "plugins/runtime") - [:> dropdown-menu-item* {:class (stl/css :menu-item) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click :on-key-down (fn [event] (when (kbd/enter? event) (on-menu-click event))) :on-pointer-enter on-menu-click - :data-testid "plugins" + :data-testid "plugins" :id "file-menu-plugins"} - [:span {:class (stl/css :item-name)} (tr "workspace.plugins.menu.title")] - [:span {:class (stl/css :open-arrow)} deprecated-icon/arrow]]) + [:span {:class (stl/css :item-name)} + (tr "workspace.plugins.menu.title")] + [:> icon* {:icon-id i/arrow-right + :class (stl/css :item-arrow)}]]) + + (when (contains? cf/flags :mcp) + (let [mcp-enabled? (-> profile :props :mcp-enabled) + mcp-connected? (-> workspace-local :mcp :connected) + mcp-active? (and mcp-enabled? mcp-connected?)] + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event))) + :on-pointer-enter on-menu-click + :data-testid "mcp" + :id "file-menu-mcp"} + [:span {:class (stl/css :item-name)} + (tr "workspace.header.menu.option.mcp")] + [:span {:class (stl/css-case :item-indicator true + :active mcp-active?)}] + [:> icon* {:icon-id i/arrow-right + :class (stl/css :item-arrow)}]])) [:div {:class (stl/css :separator)}] - [:> dropdown-menu-item* {:class (stl/css-case :menu-item true) + + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click :on-key-down (fn [event] (when (kbd/enter? event) (on-menu-click event))) :on-pointer-enter on-menu-click - :data-testid "help-info" + :data-testid "help-info" :id "file-menu-help-info"} - [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.help-info")] - [:span {:class (stl/css :open-arrow)} deprecated-icon/arrow]] + [:span {:class (stl/css :item-name)} + (tr "workspace.header.menu.option.help-info")] + [:> icon* {:icon-id i/arrow-right + :class (stl/css :item-arrow)}]] - (when (and (contains? cf/flags :subscriptions) (not= "enterprise" subscription-type)) + (when (and (contains? cf/flags :subscriptions) + (not= "enterprise" subscription-type)) [:> main-menu-power-up* {:close-sub-menu close-sub-menu}]) ;; TODO remove this block when subscriptions is full implemented (when (contains? cf/flags :subscriptions-old) - [:> dropdown-menu-item* {:class (stl/css-case :menu-item true) + [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-power-up-click :on-key-down (fn [event] (when (kbd/enter? event) (on-power-up-click))) :on-pointer-enter close-sub-menu :id "file-menu-power-up"} - [:span {:class (stl/css :item-name)} (tr "subscription.workspace.header.menu.option.power-up")]])] + [:span {:class (stl/css :item-name)} + (tr "subscription.workspace.header.menu.option.power-up")]])] - (case sub-menu + (case selected-sub-menu :file [:> file-menu* {:file file :on-close close-sub-menu}] :edit - [:> edit-menu* - {:on-close close-sub-menu}] + [:> edit-menu* {:on-close close-sub-menu}] :view - [:> view-menu* - {:layout layout - :toggle-flag toggle-flag - :on-close close-sub-menu}] + [:> view-menu* {:layout layout + :toggle-flag toggle-flag + :on-close close-sub-menu}] :preferences - [:> preferences-menu* - {:layout layout - :profile profile - :toggle-flag toggle-flag - :toggle-theme toggle-theme - :on-close close-sub-menu}] + [:> preferences-menu* {:layout layout + :profile profile + :toggle-flag toggle-flag + :toggle-theme toggle-theme + :on-close close-sub-menu}] :plugins - [:> plugins-menu* - {:open-plugins open-plugins-manager - :on-close close-sub-menu}] + [:> plugins-menu* {:open-plugins open-plugins-manager + :on-close close-sub-menu}] + + :mcp + [:> mcp-menu* {:on-close close-sub-menu}] :help-info - [:> help-info-menu* - {:layout layout - :on-close close-sub-menu}] + [:> help-info-menu* {:layout layout + :on-close close-sub-menu}] nil)])) diff --git a/frontend/src/app/main/ui/workspace/main_menu.scss b/frontend/src/app/main/ui/workspace/main_menu.scss index 7deccc70ed..d69b09e61a 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.scss +++ b/frontend/src/app/main/ui/workspace/main_menu.scss @@ -4,125 +4,174 @@ // // Copyright (c) KALEIDOS INC -@use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography.scss" as t; +@use "ds/z-index.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; + +.base-menu { + position: absolute; + display: flex; + flex-direction: column; + gap: var(--sp-xs); + padding: var(--sp-xs); + border-radius: $br-8; + z-index: var(--z-index-dropdown); + background-color: var(--menu-background-color); + border: $b-2 solid var(--panel-border-color); + box-shadow: 0 0 $sz-12 0 var(--menu-shadow-color); +} .menu { - @extend .menu-dropdown; - top: deprecated.$s-48; - left: calc(var(--right-sidebar-width, deprecated.$s-256) - deprecated.$s-16); - width: deprecated.$s-192; - margin: 0; -} - -.menu-item { - @extend .menu-item-base; - cursor: pointer; - - .open-arrow { - @include deprecated.flexCenter; - - svg { - @extend .button-icon; - stroke: var(--icon-foreground); - } - } - - &:hover { - color: var(--menu-foreground-color-hover); - - .open-arrow { - svg { - stroke: var(--menu-foreground-color-hover); - } - } - - .shortcut-key { - color: var(--menu-shortcut-foreground-color-hover); - } - } -} - -.separator { - border-top: deprecated.$s-1 solid var(--color-background-quaternary); - height: deprecated.$s-4; - left: calc(-1 * deprecated.$s-4); - margin-top: deprecated.$s-8; - position: relative; - width: calc(100% + deprecated.$s-8); -} - -.shortcut { - @extend .shortcut-base; -} - -.shortcut-key { - @extend .shortcut-key-base; + top: $sz-48; + left: calc(var(--right-sidebar-width) - $sz-40); + inline-size: $sz-192; } .sub-menu { - @extend .menu-dropdown; - left: calc(var(--right-sidebar-width, deprecated.$s-256) + deprecated.$s-180); - width: deprecated.$s-192; - min-width: calc(deprecated.$s-272 - deprecated.$s-2); - width: 110%; + left: calc(var(--right-sidebar-width) + $sz-154); + min-width: $sz-284; + width: 115%; - .submenu-item { - @extend .menu-item-base; - - &:hover { - color: var(--menu-foreground-color-hover); - - .shortcut-key { - color: var(--menu-shortcut-foreground-color-hover); - } - } + &.pos-1 { + top: calc($sz-16 + $sz-32); } - .menu-disabled { - color: var(--color-foreground-secondary); - - &:hover { - cursor: default; - color: var(--color-foreground-secondary); - background-color: var(--menu-background-color); - } + &.pos-2 { + top: calc($sz-16 + (2 * $sz-32)); } - &.file { - top: deprecated.$s-48; + &.pos-3 { + top: calc($sz-16 + (3 * $sz-32)); } - &.edit { - top: deprecated.$s-76; + &.pos-4 { + top: calc($sz-16 + (4 * $sz-32)); } - &.view { - top: deprecated.$s-116; + &.pos-5 { + top: calc($sz-16 + (5 * $sz-32)); } - &.preferences { - top: deprecated.$s-148; + &.pos-6 { + top: calc($sz-16 + (6 * $sz-32)); + } + + &.pos-final-5 { + top: calc($sz-32 + (5 * $sz-32)); + } + + &.pos-final-6 { + top: calc($sz-32 + (6 * $sz-32)); + } + + &.pos-final-7 { + top: calc($sz-32 + (7 * $sz-32)); } &.plugins { - top: deprecated.$s-180; - max-height: calc(100vh - deprecated.$s-180); + max-height: calc(100vh - $sz-200); overflow-x: hidden; overflow-y: auto; } +} - &.help-info { - top: deprecated.$s-232; +.base-menu-item { + @include t.use-typography("body-small"); + display: grid; + align-items: center; + grid-template-columns: auto $sz-16 $sz-16; + grid-template-areas: "name indicator arrow"; + block-size: $sz-28; + inline-size: 100%; + padding: $sz-6; + border-radius: $br-8; + color: var(--menu-foreground-color); + background-color: var(--menu-background-color); + + &:hover { + --menu-foreground-color: var(--menu-foreground-color-hover); + --menu-background-color: var(--menu-background-color-hover); + --menu-shortcut-foreground-color: var(--menu-shortcut-foreground-color-hover); + --menu-icon-foreground-color: var(--menu-foreground-color-hover); } - &.help-info-old { - top: deprecated.$s-192; + &.disabled { + --menu-foreground-color: var(--color-foreground-secondary); + pointer-events: none; } } +.menu-item { + display: grid; + align-items: center; + grid-template-columns: auto $sz-16 $sz-16; + grid-template-areas: "name indicator arrow"; +} + +.submenu-item { + display: flex; + align-items: center; + justify-content: space-between; +} + +.item-name { + grid-area: name; +} + +.item-indicator { + --menu-indicator-color: var(--color-foreground-secondary); + grid-area: indicator; + display: flex; + align-items: center; + justify-content: center; + inline-size: px2rem(8); + block-size: px2rem(8); + border-radius: $br-circle; + background-color: var(--menu-indicator-color); + + &.active { + --menu-indicator-color: var(--color-accent-primary); + } +} + +.item-arrow { + grid-area: arrow; + color: var(--menu-icon-foreground-color); +} + .item-icon { - svg { - @extend .button-icon; - stroke: var(--icon-foreground); - } + color: var(--menu-icon-foreground-color); + display: flex; + align-items: center; + justify-content: center; +} + +.separator { + position: relative; + block-size: var(--sp-xs); + inline-size: calc(100% + var(--sp-s)); + border-top: $b-1 solid var(--color-background-quaternary); + left: calc(-1 * var(--sp-xs)); + margin-top: var(--sp-s); +} + +.shortcut { + display: flex; + align-items: center; + justify-content: center; + gap: var(--sp-xxs); + color: var(--menu-shortcut-foreground-color); +} + +.shortcut-key { + @include t.use-typography("body-small"); + display: flex; + align-items: center; + justify-content: center; + height: px2rem(20); + padding: var(--sp-xxs) px2rem(6); + border-radius: $br-6; + background-color: var(--menu-shortcut-background-color); } diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index f7278fd650..ac219faa2f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -119,7 +119,7 @@ (mf/defc left-sidebar* {::mf/memo true} - [{:keys [layout file page-id tokens-lib active-tokens resolved-active-tokens]}] + [{:keys [layout file tokens-lib active-tokens resolved-active-tokens]}] (let [options-mode (mf/deref refs/options-mode-global) project (mf/deref refs/project) file-id (get file :id) @@ -185,12 +185,10 @@ :class aside-class :style {:--left-sidebar-width (dm/str width "px")}} - [:> left-header* - {:file file - :layout layout - :project project - :page-id page-id - :class (stl/css :left-header)}] + [:> left-header* {:file file + :layout layout + :project project + :class (stl/css :left-header)}] [:div {:on-pointer-down on-pointer-down :on-lost-pointer-capture on-lost-pointer-capture diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 0b88c9cf94..e29a378cbe 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5719,6 +5719,18 @@ msgstr "Hide rulers" msgid "workspace.header.menu.hide-textpalette" msgstr "Hide fonts palette" +msgid "workspace.header.menu.mcp.plugin.status.connect" +msgstr "Connect" + +msgid "workspace.header.menu.mcp.plugin.status.disconnect" +msgstr "Disconnect" + +msgid "workspace.header.menu.mcp.server.status.enabled" +msgstr "Manage (Status: enabled)" + +msgid "workspace.header.menu.mcp.server.status.disabled" +msgstr "Manage (Status: disabled)" + #: src/app/main/ui/workspace/main_menu.cljs:884 msgid "workspace.header.menu.option.edit" msgstr "Edit" @@ -5731,6 +5743,9 @@ msgstr "File" msgid "workspace.header.menu.option.help-info" msgstr "Help & info" +msgid "workspace.header.menu.option.mcp" +msgstr "MCP server" + #: src/app/main/ui/workspace/main_menu.cljs:916 #, unused msgid "workspace.header.menu.option.power-up" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 37b55a8deb..a3123f8289 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5686,6 +5686,18 @@ msgstr "Ocultar reglas" msgid "workspace.header.menu.hide-textpalette" msgstr "Ocultar paleta de textos" +msgid "workspace.header.menu.mcp.plugin.status.connect" +msgstr "Conectar" + +msgid "workspace.header.menu.mcp.plugin.status.disconnect" +msgstr "Desconectar" + +msgid "workspace.header.menu.mcp.server.status.enabled" +msgstr "Gestionar (estado: habilitado)" + +msgid "workspace.header.menu.mcp.server.status.disabled" +msgstr "Gestionar (estado: deshabilitado)" + #: src/app/main/ui/workspace/main_menu.cljs:884 msgid "workspace.header.menu.option.edit" msgstr "Editar" @@ -5698,6 +5710,9 @@ msgstr "Archivo" msgid "workspace.header.menu.option.help-info" msgstr "Ayuda e información" +msgid "workspace.header.menu.option.mcp" +msgstr "Servidor MCP" + #: src/app/main/ui/workspace/main_menu.cljs:906 msgid "workspace.header.menu.option.preferences" msgstr "Preferencias"