diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index f31016ead4..891b2a4e31 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -448,33 +448,6 @@ export class WorkspacePage extends BaseWebSocketPage { await pagesToggle.click(); } - async selectToolbarTool(workspacePage, toolName) { - await workspacePage.page - .getByRole("button", { name: toolName }) - .first() - .click(); - } - - async selectToolFromFlyout( - workspacePage, - { triggerToolName, targetToolName }, - ) { - const trigger = workspacePage.page - .getByRole("button", { name: triggerToolName }) - .first(); - - const option = workspacePage.page - .getByRole("menuitemradio", { name: targetToolName }) - .first(); - - await trigger.hover(); - // Flyout opening is delayed by 350ms in the toolbar component. - await workspacePage.page.waitForTimeout(450); - await expect(trigger).toHaveAttribute("aria-expanded", "true"); - await option.waitFor({ state: "visible" }); - await option.click(); - } - async moveSelectionToShape(name) { await this.page.locator("rect.viewport-selrect").hover(); await this.page.mouse.down(); diff --git a/frontend/playwright/ui/specs/toolbar.spec.js b/frontend/playwright/ui/specs/toolbar.spec.js deleted file mode 100644 index 2bd9cef5c4..0000000000 --- a/frontend/playwright/ui/specs/toolbar.spec.js +++ /dev/null @@ -1,88 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; -import WorkspacePage from "../pages/WorkspacePage"; - -test.beforeEach(async ({ page }) => { - await WasmWorkspacePage.init(page); -}); - -const expectLayerNamed = async (workspacePage, name) => { - await expect(workspacePage.layers.getByText(name).last()).toBeVisible(); -}; - -test("User creates a frame with the toolbar frame tool", async ({ page }) => { - const workspacePage = new WasmWorkspacePage(page); - await workspacePage.setupEmptyFile(); - await workspacePage.goToWorkspace(); - - await workspacePage.selectToolbarTool(workspacePage, "Board (B)"); - await workspacePage.clickWithDragViewportAt(100, 100, 180, 120); - await expectLayerNamed(workspacePage, "Board"); -}); - -test("User creates a rectangle with the toolbar rect tool", async ({ - page, -}) => { - const workspacePage = new WasmWorkspacePage(page); - await workspacePage.setupEmptyFile(); - await workspacePage.goToWorkspace(); - - await workspacePage.selectToolbarTool(workspacePage, "Rectangle (R)"); - await workspacePage.clickWithDragViewportAt(350, 100, 120, 80); - await expectLayerNamed(workspacePage, "Rectangle"); -}); - -test("User creates an ellipse from the shapes flyout", async ({ page }) => { - const workspacePage = new WasmWorkspacePage(page); - await workspacePage.setupEmptyFile(); - await workspacePage.goToWorkspace(); - - await workspacePage.selectToolFromFlyout(workspacePage, { - triggerToolName: "Rectangle (R)", - targetToolName: "Ellipse (E)", - }); - await workspacePage.clickWithDragViewportAt(520, 100, 100, 100); - await expectLayerNamed(workspacePage, "Ellipse"); -}); - -test("User creates a text shape with the toolbar text tool", async ({ - page, -}) => { - const workspacePage = new WorkspacePage(page); - await workspacePage.setupEmptyFile(); - await workspacePage.goToWorkspace(); - - await workspacePage.selectToolbarTool(workspacePage, "Text (T)"); - await workspacePage.clickAndMove(120, 320, 300, 380); - await workspacePage.waitForSelectedShapeName("Text"); - await workspacePage.page.keyboard.type("toolbar test"); - await workspacePage.page.keyboard.press("Escape"); - await expectLayerNamed(workspacePage, "Text"); -}); - -test.skip("User creates a path with the toolbar path tool", async ({ - page, -}) => { - const workspacePage = new WasmWorkspacePage(page); - await workspacePage.setupEmptyFile(); - await workspacePage.goToWorkspace(); - - await workspacePage.selectToolbarTool(workspacePage, "Path (P)"); - await workspacePage.clickAndMove(120, 320, 300, 380); - await workspacePage.page.keyboard.press("Enter"); - await expectLayerNamed(workspacePage, "Path"); -}); - -test("User creates a curve from the path flyout", async ({ page }) => { - const workspacePage = new WasmWorkspacePage(page); - await workspacePage.setupEmptyFile(); - await workspacePage.goToWorkspace(); - - await workspacePage.selectToolFromFlyout(workspacePage, { - triggerToolName: "Path (P)", - targetToolName: "Curve (Shift+C)", - }); - await workspacePage.clickAndMove(120, 320, 300, 380); - await workspacePage.page.keyboard.press("Enter"); - await expectLayerNamed(workspacePage, "Path"); -}); diff --git a/frontend/playwright/ui/specs/variants.spec.js b/frontend/playwright/ui/specs/variants.spec.js index d9dbb687e5..0c50f7dc8b 100644 --- a/frontend/playwright/ui/specs/variants.spec.js +++ b/frontend/playwright/ui/specs/variants.spec.js @@ -343,9 +343,7 @@ test("User drag and drop a variant outside the container", async ({ page }) => { // and use it to calculate the target position await workspacePage.clickWithDragViewportAt(600, 500, 0, 300); - await expect( - workspacePage.layers.getByText("Rectangle / Value 1"), - ).toBeVisible(); + await expect(workspacePage.layers.getByText("Rectangle / Value 1")).toBeVisible(); }); test("User cut paste a component inside a variant", async ({ page }) => { @@ -355,10 +353,7 @@ test("User cut paste a component inside a variant", async ({ page }) => { const variant = await findVariant(workspacePage, 0); //Create a component - await workspacePage.selectToolFromFlyout(workspacePage, { - triggerToolName: "Rectangle (R)", - targetToolName: "Ellipse (E)", - }); + await workspacePage.ellipseShapeButton.click(); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.page.keyboard.press("ControlOrMeta+k"); @@ -389,10 +384,7 @@ test("User cut paste a component with path inside a variant", async ({ const variant = await findVariant(workspacePage, 0); // Create a component - await workspacePage.selectToolFromFlyout(workspacePage, { - triggerToolName: "Rectangle (R)", - targetToolName: "Ellipse (E)", - }); + await workspacePage.ellipseShapeButton.click(); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.page.keyboard.press("ControlOrMeta+k"); @@ -433,10 +425,7 @@ test("User drag and drop a component with path inside a variant", async ({ const variant = findVariantNoWait(workspacePage, 0); //Create a component - await workspacePage.selectToolFromFlyout(workspacePage, { - triggerToolName: "Rectangle (R)", - targetToolName: "Ellipse (E)", - }); + await workspacePage.ellipseShapeButton.click(); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.page.keyboard.press("ControlOrMeta+k"); @@ -468,10 +457,7 @@ test("User cut paste a variant into another container", async ({ page }) => { await setupVariantsFileWithVariant(workspacePage); // Create anothe variant - await workspacePage.selectToolFromFlyout(workspacePage, { - triggerToolName: "Rectangle (R)", - targetToolName: "Ellipse (E)", - }); + await workspacePage.ellipseShapeButton.click(); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.page.keyboard.press("ControlOrMeta+k"); diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs index 272d4288a3..956bc44f35 100644 --- a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs +++ b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs @@ -23,49 +23,22 @@ [:icon [:and :string [:fn #(contains? icon-list %)]]] [:aria-label :string] - [:has-tooltip {:optional true} [:maybe :boolean]] [:tooltip-placement {:optional true} [:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]] - ;; Indicates that the button has a flyout menu, and should display an indicator - [:flyout-indicator {:optional true} [:maybe :boolean]] [:variant {:optional true} [:maybe [:enum "primary" "secondary" "ghost" "destructive" "action"]]]]) -(def ^:private schema:icon-button-internal - [:map - [:icon-class {:optional true} [:maybe :string]] - [:icon-size {:optional true} [:maybe [:enum "s" "m" "l"]]] - [:icon - [:and :string [:fn #(contains? icon-list %)]]] - ;; Indicates that the button has a flyout menu, and should display an indicator - [:flyout-indicator {:optional true} [:maybe :boolean]]]) - -(mf/defc icon-button-internal* - {::mf/schema schema:icon-button-internal - ::mf/memo true} - [{:keys [icon icon-class icon-size flyout-indicator children] :rest props}] - [:> :button props - [:> icon* {:icon-id icon - :aria-hidden true - :class icon-class - :size icon-size}] - (when flyout-indicator - [:svg {:view-box "0 0 6 6" - :aria-hidden true - :class (stl/css :flyout-indicator)} - [:path {:d "M4,2 L4,3.15 C4,3.62 3.62,4 3.15,4 L2,4" - :stroke-linecap "round"}]]) - children]) - (mf/defc icon-button* {::mf/schema schema:icon-button ::mf/memo true} - [{:keys [class icon icon-class icon-size variant aria-label children tooltip-placement tooltip-class type flyout-indicator has-tooltip] :rest props}] - (let [variant (d/nilv variant "primary") - flyout-indicator (d/nilv flyout-indicator false) - has-tooltip (d/nilv has-tooltip true) + [{:keys [class icon icon-class icon-size variant aria-label children tooltip-placement tooltip-class type] :rest props}] + (let [variant + (d/nilv variant "primary") + button-ref (mf/use-ref nil) - tooltip-id (mf/use-id) + + tooltip-id + (mf/use-id) button-class (stl/css-case :icon-button true @@ -80,19 +53,13 @@ {:class [class button-class] :ref button-ref :type (d/nilv type "button") - :icon icon - :icon-size icon-size - :icon-class icon-class - :flyout-indicator flyout-indicator})] + :aria-labelledby tooltip-id})] - (if has-tooltip - (let [tooltip-props (mf/spread-props props {:aria-labelledby tooltip-id})] - [:> tooltip* {:content aria-label - :class tooltip-class - :trigger-ref button-ref - :placement tooltip-placement - :id tooltip-id} - [:> icon-button-internal* tooltip-props children]]) - - (let [no-tooltip-props (mf/spread-props props {:aria-label aria-label})] - [:> icon-button-internal* no-tooltip-props children])))) + [:> tooltip* {:content aria-label + :class tooltip-class + :trigger-ref button-ref + :placement tooltip-placement + :id tooltip-id} + [:> :button props + [:> icon* {:icon-id icon :aria-hidden true :class icon-class :size icon-size}] + children]])) diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.scss b/frontend/src/app/main/ui/ds/buttons/icon_button.scss index 233bb8bfe7..27f66f3a19 100644 --- a/frontend/src/app/main/ui/ds/buttons/icon_button.scss +++ b/frontend/src/app/main/ui/ds/buttons/icon_button.scss @@ -16,7 +16,6 @@ display: grid; place-content: center; - position: relative; } .icon-button-primary { @@ -50,13 +49,3 @@ --button-width: #{$sz-24}; --button-height: #{$sz-24}; } - -.flyout-indicator { - position: absolute; - inset-block-end: var(--sp-xs); - inset-inline-end: var(--sp-xs); - stroke: currentcolor; - fill: none; - inline-size: $sz-6; - block-size: $sz-6; -} diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.stories.jsx b/frontend/src/app/main/ui/ds/buttons/icon_button.stories.jsx index de37e42a02..fd90936d34 100644 --- a/frontend/src/app/main/ui/ds/buttons/icon_button.stories.jsx +++ b/frontend/src/app/main/ui/ds/buttons/icon_button.stories.jsx @@ -24,7 +24,6 @@ export default { options: iconList, control: { type: "select" }, }, - flyoutIndicator: { control: "boolean" }, disabled: { control: "boolean" }, variant: { options: ["primary", "secondary", "ghost", "destructive", "action"], @@ -36,7 +35,6 @@ export default { variant: undefined, "aria-label": "Lorem ipsum", icon: "effects", - flyoutIndicator: false, }, render: ({ ...args }) => , }; @@ -72,10 +70,3 @@ export const Destructive = { variant: "destructive", }, }; - -export const Flyout = { - args: { - variant: "ghost", - flyoutIndicator: true, - }, -}; diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.cljs b/frontend/src/app/main/ui/workspace/top_toolbar.cljs index 615e0921b1..dcce4fecc6 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/top_toolbar.cljs @@ -20,172 +20,79 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]] - [app.main.ui.components.file-uploader :as file-uploader] + [app.main.ui.components.file-uploader :refer [file-uploader]] [app.main.ui.context :as ctx] - [app.main.ui.ds.buttons.button :refer [button*]] - [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] - [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] - [app.util.i18n :refer [tr]] + [app.util.i18n :as i18n :refer [tr]] [app.util.timers :as ts] [okulary.core :as l] [rumext.v2 :as mf])) -(def ^:private toolbar-hidden-ref - (l/derived (fn [state] - (let [visibility (get state :hide-toolbar) - path-edit-state (get state :edit-path) - selected (get state :selected) - edition (get state :edition) +(mf/defc mcp-indicator* + [] + (let [mcp (mf/deref refs/mcp) - is-single (= (count selected) 1) - is-path-editing (and is-single (some? (get path-edit-state edition)))] + conn-status (get mcp :connection-status) + has-valid-token? (get mcp :token-valid) - (if is-path-editing true visibility))) - refs/workspace-local)) + enabled? (get mcp :enabled) -(def grouped-tools - {:shapes {:default-tool :rect - :tools {:rect {:icon i/rectangle} - :circle {:icon i/ellipse}}} - :free-draw {:default-tool :path - :tools {:path {:icon i/path} - :curve {:icon i/curve}}}}) + mcp-connected? (= "connected" conn-status) + show-indicator? (and enabled? has-valid-token?) -(defn- tool-label - [tool] - (case tool - :move (tr "workspace.toolbar.move" (sc/get-tooltip :move)) - :frame (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame)) - :rect (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect)) - :circle (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse)) - :text (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text)) - :path (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path)) - :image (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image)) - :curve (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve)) - :plugins (tr "workspace.toolbar.plugins" (sc/get-tooltip :plugins)) - :debug "Debugging tool" - (name tool))) + menu-open* (mf/use-state false) + menu-open? (deref menu-open*) -(defn- active-group-tool - [group drawtool] - (if (contains? (:tools group) drawtool) - drawtool - (:default-tool group))) - -(defn- is-selected-group - [group drawtool] - (contains? (:tools group) drawtool)) - -(defn- group-menu-label - [group drawtool] - (let [tool-id (active-group-tool group drawtool)] - (str (tr "labels.options") ": " (tool-label tool-id)))) - -(defn- cancel-timer! - [timer-ref*] - (when-let [timer (mf/ref-val timer-ref*)] - (ts/dispose! timer) - (mf/set-ref-val! timer-ref* nil))) - -(mf/defc group-tool* - {::mf/private true - ::mf/wrap [mf/memo]} - [{:keys [group drawtool on-select-tool]}] - (let [default-tool* (mf/use-state (active-group-tool group drawtool)) - default-tool (deref default-tool*) - - open* (mf/use-state false) - open (deref open*) - - open-timer* (mf/use-ref nil) - close-timer* (mf/use-ref nil) - - default-icon (:icon (get-in group [:tools default-tool])) - subtools (:tools group) - menu-label (group-menu-label group drawtool) - selected (boolean (is-selected-group group drawtool)) - - on-select-tool + toggle-menu (mf/use-fn (fn [event] - (let [tool (-> (dom/get-current-target event) - (dom/get-data "tool") - (keyword))] - (reset! default-tool* tool) - (on-select-tool event)))) + (dom/stop-propagation event) + (swap! menu-open* not))) - on-display-menu + close-menu (mf/use-fn - (fn [] - (cancel-timer! close-timer*) - (cancel-timer! open-timer*) - (mf/set-ref-val! - open-timer* - (ts/schedule 350 - #(do - (reset! open* true) - (mf/set-ref-val! open-timer* nil)))))) + #(reset! menu-open* false)) - on-hide-menu + connect-mcp (mf/use-fn - (fn [] - (cancel-timer! open-timer*) - (cancel-timer! close-timer*) - (mf/set-ref-val! - close-timer* - (ts/schedule 350 - #(do - (reset! open* false) - (mf/set-ref-val! close-timer* nil))))))] + #(st/emit! (mcp/connect-mcp) + (ev/event {::ev/name "connect-mcp-plugin" + ::ev/origin "workspace:toolbar"})))] + (when show-indicator? + [:li + [:button + {:title (tr "workspace.toolbar.mcp") + :aria-label (tr "workspace.toolbar.mcp") + :class (stl/css-case :main-toolbar-options-button true + :mcp-button true + :selected menu-open?) + :on-click toggle-menu + :data-tool "mcp" + :data-testid "mcp-btn"} + [:span {:class (stl/css-case :mcp-status-dot true + :connected mcp-connected?)}] + [:span {:class (stl/css-case :mcp-button-label true + :connected mcp-connected?)} + (tr "workspace.toolbar.mcp")]] + [:> dropdown-menu* {:show menu-open? + :on-close close-menu + :class (stl/css :mcp-menu)} + (if mcp-connected? + [:li {:class (stl/css :mcp-menu-info) + :role "presentation"} + (tr "workspace.toolbar.mcp-connected")] + [:> dropdown-menu-item* {:class (stl/css :mcp-menu-item) + :on-click connect-mcp} + (tr "workspace.toolbar.mcp-connect-here")])]]))) - (mf/with-effect [] - (fn [] - (cancel-timer! open-timer*) - (cancel-timer! close-timer*))) - - [:li {:class (stl/css :toolbar-group) - :on-pointer-enter on-display-menu - :on-pointer-leave on-hide-menu} - [:div {:role "group" - :aria-label menu-label} - [:> icon-button* {:variant "ghost" - :flyout-indicator true - :aria-label (tool-label default-tool) - :aria-haspopup true - :aria-pressed selected - :aria-expanded open - :has-tooltip false - :icon default-icon - :on-click on-select-tool - :data-tool (name default-tool)}] - - [:ul {:role "menu" - :class (stl/css-case :toolbar-group-flyout true - :open open) - :aria-label menu-label} - - (for [[id {:keys [icon]}] subtools] - [:li {:key (name id) - :role "none"} - [:> icon-button* {:variant "ghost" - :tooltip-placement "bottom" - :role "menuitemradio" - :aria-label (tool-label id) - :aria-pressed (= drawtool id) - :aria-checked (= drawtool id) - :icon icon - :on-click on-select-tool - :data-tool (name id)}]])]]])) - -(mf/defc image-upload-tool* - {::mf/private true - ::mf/wrap [mf/memo]} +(mf/defc image-upload* + {::mf/wrap [mf/memo]} [] - (let [ref (mf/use-ref nil) - file-id (mf/use-ctx ctx/current-file-id) + (let [ref (mf/use-ref nil) + file-id (mf/use-ctx ctx/current-file-id) - on-display-uploader + on-click (mf/use-fn (fn [] (st/emit! :interrupt (dw/clear-edition-mode)) @@ -204,115 +111,50 @@ :blobs (seq blobs) :position (gpt/point x y)}] (st/emit! (dwm/upload-media-workspace params)))))] + [:li + [:button + {:title (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image)) + :aria-label (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image)) + :on-click on-click + :class (stl/css :main-toolbar-options-button)} + deprecated-icon/img + [:& file-uploader + {:input-id "image-upload" + :accept dwm/accept-image-types + :multi true + :ref ref + :on-selected on-selected}]]])) - [:* - [:> icon-button* {:variant "ghost" - :tooltip-placement "bottom" - :aria-label (tool-label :image) - :icon i/img - :on-click on-display-uploader}] - [:& file-uploader/file-uploader {:input-id "image-upload" - :accept dwm/accept-image-types - :multi true - :ref ref - :on-selected on-selected}]])) +(def ^:private toolbar-hidden-ref + (l/derived (fn [state] + (let [visibility (get state :hide-toolbar) + path-edit-state (get state :edit-path) -(mf/defc mcp-tool* - {::mf/private true - ::mf/wrap [mf/memo]} - [{:keys [is-mcp-connected]}] - (let [menu-open* (mf/use-state false) - menu-open? (deref menu-open*) + selected (get state :selected) + edition (get state :edition) + single? (= (count selected) 1) - on-toggle-menu - (mf/use-fn - (fn [event] - (dom/stop-propagation event) - (swap! menu-open* not))) - - on-close-menu - (mf/use-fn - #(reset! menu-open* false)) - - on-connect - (mf/use-fn - #(st/emit! (mcp/connect-mcp) - (ev/event {::ev/name "connect-mcp-plugin" - ::ev/origin "workspace:toolbar"})))] - - [:* - [:> button* {:variant "ghost" - :on-click on-toggle-menu - :aria-pressed menu-open? - :data-tool "mcp" - :data-testid "mcp-btn"} - [:div {:class (stl/css-case :toolbar-mcp-button true - :selected menu-open?)} - [:span {:class (stl/css-case :toolbar-mcp-button-dot true - :connected is-mcp-connected)}] - [:span {:class (stl/css-case :toolbar-mcp-button-label true - :connected is-mcp-connected)} - (tr "workspace.toolbar.mcp")]]] - - [:div {:class (stl/css :toolbar-mcp-menu)} - [:> dropdown-menu* {:show menu-open? - :on-close on-close-menu - :class (stl/css :toolbar-mcp-dropdown)} - (if is-mcp-connected - [:li {:class (stl/css :toolbar-mcp-dropdown-info) - :role "presentation"} - (tr "workspace.toolbar.mcp-connected")] - [:> dropdown-menu-item* {:class (stl/css :toolbar-mcp-dropdown-item) - :on-click on-connect} - (tr "workspace.toolbar.mcp-connect-here")])]]])) + path-editing? (and single? (some? (get path-edit-state edition)))] + (if path-editing? true visibility))) + refs/workspace-local)) (mf/defc top-toolbar* - {::mf/wrap [mf/memo]} + {::mf/memo true} [{:keys [layout]}] - (let [selected-drawing-tool (mf/deref refs/selected-drawing-tool) - selected-edition (mf/deref refs/selected-edition) - rulers-enabled (mf/deref refs/rulers?) - toolbar-hidden (mf/deref toolbar-hidden-ref) - mcp (mf/deref refs/mcp) + (let [drawtool (mf/deref refs/selected-drawing-tool) + edition (mf/deref refs/selected-edition) - plugins-enabled? (features/active-feature? @st/state "plugins/runtime") - read-only? (mf/use-ctx ctx/workspace-read-only?) + profile (mf/deref refs/profile) + props (get profile :props) - mcp-conn-status (get mcp :connection-status) - mcp-valid-token? (get mcp :token-valid) - mcp-enabled? (get mcp :enabled) + read-only? (mf/use-ctx ctx/workspace-read-only?) + rulers? (mf/deref refs/rulers?) + hide-toolbar? (mf/deref toolbar-hidden-ref) - mcp-connected? (= "connected" mcp-conn-status) - mcp-show? (and (contains? cf/flags :mcp) - mcp-enabled? - mcp-valid-token?) + interrupt + (mf/use-fn #(st/emit! :interrupt (dw/clear-edition-mode))) - separator? (or plugins-enabled? *assert* mcp-show?) - - on-display-plugins-manager - (mf/use-fn - (fn [] - (st/emit! (ev/event {::ev/name "open-plugins-manager" - ::ev/origin "workspace:toolbar"}) - (modal/show :plugin-management {})))) - - on-toggle-debug-panel - (mf/use-fn - (mf/deps layout) - (fn [] - (let [is-sidebar-closed (contains? layout :collapse-left-sidebar)] - (when is-sidebar-closed - (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) - (st/emit! (dw/remove-layout-flag :shortcuts) - (-> (dw/toggle-layout-flag :debug-panel) - (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))) - - on-interrupt - (mf/use-fn - (fn [] - (st/emit! :interrupt (dw/clear-edition-mode)))) - - on-select-tool + select-drawtool (mf/use-fn (fn [event] (let [tool (-> (dom/get-current-target event) @@ -321,91 +163,131 @@ (st/emit! :interrupt (dw/clear-edition-mode)) ;; Delay so anything that launched :interrupt can finish - (ts/schedule 100 - #(st/emit! (dw/select-for-drawing tool)))))) + (ts/schedule 100 #(st/emit! (dw/select-for-drawing tool)))))) - on-toggle-toolbar + toggle-debug-panel + (mf/use-fn + (mf/deps layout) + (fn [] + (let [is-sidebar-closed? (contains? layout :collapse-left-sidebar)] + (when is-sidebar-closed? + (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) + (st/emit! + (dw/remove-layout-flag :shortcuts) + (-> (dw/toggle-layout-flag :debug-panel) + (vary-meta assoc ::ev/origin "workspace-left-toolbar")))))) + + toggle-toolbar (mf/use-fn (fn [event] (dom/blur! (dom/get-target event)) - (st/emit! (dwc/toggle-toolbar-visibility))))] + (st/emit! (dwc/toggle-toolbar-visibility)))) + + test-tooltip-board-text + (if (not (:workspace-visited props)) + (tr "workspace.toolbar.frame-first-time" (sc/get-tooltip :draw-frame)) + (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame)))] (when-not ^boolean read-only? - [:div {:role "toolbar" - :aria-label (tr "workspace.toolbar.label") - :tabindex "0" - :class (stl/css-case :toolbar true - :no-rulers (not rulers-enabled) - :hidden toolbar-hidden)} - [:ul {:class (stl/css :toolbar-options) + [:aside {:class (stl/css-case :main-toolbar true + :main-toolbar-no-rulers (not rulers?) + :main-toolbar-hidden hide-toolbar?)} + [:ul {:class (stl/css :main-toolbar-options) :data-testid "toolbar-options"} - [:li {:class (stl/css :toolbar-option)} - [:> icon-button* {:variant "ghost" - :tooltip-placement "bottom" - :aria-pressed (and (nil? selected-drawing-tool) - (not selected-edition)) - :aria-label (tr "workspace.toolbar.move" (sc/get-tooltip :move)) - :icon i/move - :on-click on-interrupt}]] + [:li + [:button + {:title (tr "workspace.toolbar.move" (sc/get-tooltip :move)) + :aria-label (tr "workspace.toolbar.move" (sc/get-tooltip :move)) + :class (stl/css-case :main-toolbar-options-button true + :selected (and (nil? drawtool) + (not edition))) + :on-click interrupt} + deprecated-icon/move]] + [:* + [:li + [:button + {:title test-tooltip-board-text + :aria-label (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame)) + :class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :frame)) + :on-click select-drawtool + :data-tool "frame" + :data-testid "artboard-btn"} + deprecated-icon/board]] + [:li + [:button + {:title (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect)) + :aria-label (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect)) + :class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :rect)) + :on-click select-drawtool + :data-tool "rect" + :data-testid "rect-btn"} + deprecated-icon/rectangle]] + [:li + [:button + {:title (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse)) + :aria-label (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse)) + :class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :circle)) + :on-click select-drawtool + :data-tool "circle" + :data-testid "ellipse-btn"} + deprecated-icon/ellipse]] + [:li + [:button + {:title (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text)) + :aria-label (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text)) + :class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :text)) + :on-click select-drawtool + :data-tool "text"} + deprecated-icon/text]] - [:li {:class (stl/css :toolbar-option)} - [:> icon-button* {:variant "ghost" - :tooltip-placement "bottom" - :aria-pressed (= selected-drawing-tool :frame) - :aria-label (tool-label :frame) - :icon i/board - :on-click on-select-tool - :data-tool "frame"}]] + [:> image-upload*] - [:> group-tool* {:key :shapes - :group (get grouped-tools :shapes) - :drawtool selected-drawing-tool - :on-select-tool on-select-tool}] + [:li + [:button + {:title (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve)) + :aria-label (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve)) + :class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :curve)) + :on-click select-drawtool + :data-tool "curve" + :data-testid "curve-btn"} + deprecated-icon/curve]] + [:li + [:button + {:title (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path)) + :aria-label (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path)) + :class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :path)) + :on-click select-drawtool + :data-tool "path" + :data-testid "path-btn"} + deprecated-icon/path]] - [:li {:class (stl/css :toolbar-option)} - [:> icon-button* {:variant "ghost" - :tooltip-placement "bottom" - :aria-pressed (= selected-drawing-tool :text) - :aria-label (tool-label :text) - :icon i/text - :on-click on-select-tool - :data-tool "text"}]] + (when (features/active-feature? @st/state "plugins/runtime") + [:li + [:button + {:title (tr "workspace.toolbar.plugins" (sc/get-tooltip :plugins)) + :aria-label (tr "workspace.toolbar.plugins" (sc/get-tooltip :plugins)) + :class (stl/css :main-toolbar-options-button) + :on-click #(st/emit! + (ev/event {::ev/name "open-plugins-manager" + ::ev/origin "workspace:toolbar"}) + (modal/show :plugin-management {})) + :data-tool "plugins" + :data-testid "plugins-btn"} + deprecated-icon/puzzle]]) - [:li {:class (stl/css :toolbar-option)} - [:> image-upload-tool*]] + (when *assert* + [:li + [:button + {:title "Debugging tool" + :class (stl/css-case :main-toolbar-options-button true :selected (contains? layout :debug-panel)) + :on-click toggle-debug-panel} + deprecated-icon/bug]]) - [:> group-tool* {:key :free-draw - :group (get grouped-tools :free-draw) - :drawtool selected-drawing-tool - :on-select-tool on-select-tool}] - - (when separator? - [:div {:class (stl/css :toolbar-separator)}]) - - (when plugins-enabled? - [:li {:class (stl/css :toolbar-option)} - [:> icon-button* {:variant "ghost" - :tooltip-placement "bottom" - :aria-label (tool-label :plugins) - :icon i/puzzle - :on-click on-display-plugins-manager - :data-tool "plugins"}]]) - - (when *assert* - [:li {:class (stl/css :toolbar-option)} - [:> icon-button* {:variant "ghost" - :tooltip-placement "bottom" - :aria-pressed (contains? layout :debug-panel) - :aria-label (tool-label :debug) - :icon i/bug - :on-click on-toggle-debug-panel}]]) - - (when mcp-show? - [:li {:class (stl/css :toolbar-option)} - [:> mcp-tool* {:is-mcp-connected mcp-connected?}]])] + (when (contains? cf/flags :mcp) + [:> mcp-indicator*])]] [:button {:title (tr "workspace.toolbar.toggle-toolbar") :aria-label (tr "workspace.toolbar.toggle-toolbar") :class (stl/css :toolbar-handler) - :on-click on-toggle-toolbar} - [:div {:class (stl/css :toolbar-handler-indicator)}]]]))) + :on-click toggle-toolbar} + [:div {:class (stl/css :toolbar-handler-btn)}]]]))) diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.scss b/frontend/src/app/main/ui/workspace/top_toolbar.scss index 0db0002fbf..7a185eb692 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.scss +++ b/frontend/src/app/main/ui/workspace/top_toolbar.scss @@ -4,118 +4,109 @@ // // Copyright (c) KALEIDOS INC Sucursal en EspaƱa SL +@use "refactor/common-refactor.scss" as deprecated; @use "ds/_borders.scss" as *; -@use "ds/_sizes.scss" as *; @use "ds/_utils.scss" as *; -@use "ds/spacing.scss" as *; @use "ds/typography.scss" as t; -@use "ds/z-index.scss" as *; -.toolbar { - --toolbar-position-y: #{$sz-28}; - --toolbar-offset-y: 0px; - --menu-border-color: var(--color-background-quaternary); - --menu-background-color: var(--color-background-primary); - - cursor: default; +.main-toolbar { + cursor: initial; position: absolute; - inset-inline-start: 50%; + left: 50%; transform: translateX(-50%); display: flex; align-items: center; flex-direction: column; - padding: var(--sp-m); - border-radius: $br-8; - border: $b-2 solid var(--menu-border-color); - z-index: var(--z-index-panels); - background-color: var(--menu-background-color); + height: deprecated.$s-56; + padding: deprecated.$s-8 deprecated.$s-16; + border-radius: deprecated.$s-8; + border: deprecated.$s-2 solid var(--panel-border-color); + z-index: deprecated.$z-index-1; + background-color: var(--color-background-primary); transition: - inset-block-start 0.3s, - block-size 0.3s, + top 0.3s, + height 0.3s, opacity 0.3s; - inset-block-start: calc(var(--toolbar-position-y) + var(--toolbar-offset-y)); - will-change: auto; - &.no-rulers { - --toolbar-position-y: 0px; - --toolbar-offset-y: 8px; - } + --toolbar-position-y: #{deprecated.$s-28}; + --toolbar-offset-y: 0px; - &.hidden { - --toolbar-offset-y: -4px; + top: calc(var(--toolbar-position-y) + var(--toolbar-offset-y)); +} - block-size: $sz-16; - z-index: 1; - border-radius: 0 0 $br-8 $br-8; - border-block-start: 0; +.main-toolbar-no-rulers { + --toolbar-position-y: 0px; + --toolbar-offset-y: #{deprecated.$s-8}; +} - .toolbar-options { - opacity: 0; - visibility: hidden; - } +.main-toolbar-hidden { + --toolbar-offset-y: calc(-1 * #{deprecated.$s-4}); + + height: deprecated.$s-16; + z-index: deprecated.$z-index-1; + border-radius: 0 0 deprecated.$s-8 deprecated.$s-8; + border-block-start: 0; + + .main-toolbar-options { + opacity: deprecated.$op-0; + visibility: hidden; } } -.toolbar-options { +.main-toolbar-options { position: relative; display: flex; align-items: center; - gap: var(--sp-xs); margin: 0; - opacity: 1; + opacity: deprecated.$op-10; transition: opacity 0.3s ease; -} -.toolbar-option { - position: relative; -} - -.toolbar-group { - position: relative; -} - -.toolbar-group-flyout { - opacity: 0; - visibility: hidden; - pointer-events: none; - position: absolute; - inset-block-start: 140%; - inset-inline-start: 50%; - transform: translateX(-50%); - margin-block-start: var(--sp-xxs); - padding: var(--sp-xxs) 0; - border-radius: $br-8; - border: #{$b-1} solid var(--menu-border-color); - background-color: var(--menu-background-color); - z-index: var(--z-index-panels); - display: flex; - transition: - opacity 80ms ease-out, - visibility 80ms linear; - - &.open { - opacity: 1; - transition-delay: 0s; - visibility: visible; - pointer-events: auto; + li { + position: relative; } } -.toolbar-mcp-button { +.main-toolbar-options-button { + @extend %button-tertiary; + + height: deprecated.$s-36; + width: deprecated.$s-36; + flex-shrink: 0; + border-radius: deprecated.$s-8; + margin: 0 deprecated.$s-2; + + svg { + @extend %button-icon; + + stroke: var(--color-foreground-secondary); + } + + &.selected { + @extend %button-icon-selected; + } +} + +.mcp-button { display: flex; align-items: center; gap: var(--sp-xs); + width: fit-content; + margin-inline-start: var(--sp-xs); + padding-inline: var(--sp-s); } -.toolbar-mcp-button-label { +.mcp-button-label { + @include t.use-typography("body-small"); + &.connected { color: var(--color-accent-primary); } } -.toolbar-mcp-button-dot { +.mcp-status-dot { + // Connection indicator placed before the label, vertically centered: + // a muted gray when disconnected, the primary accent when connected. position: relative; - display: flex; flex-shrink: 0; inline-size: px2rem(6); block-size: px2rem(6); @@ -152,23 +143,23 @@ } } -.toolbar-mcp-menu { - position: absolute; - left: 0; - top: $sz-36; -} +.mcp-menu { + @include deprecated.menu-shadow; -.toolbar-mcp-dropdown { - box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark); + position: absolute; + inset-block-start: calc(100% + var(--sp-xs)); + inset-inline-start: var(--sp-xs); z-index: var(--z-index-dropdown); margin: 0; padding: var(--sp-xs); + list-style: none; border: $b-1 solid var(--panel-border-color); border-radius: $br-8; background-color: var(--menu-background-color); } -.toolbar-mcp-dropdown-info { +// Non-interactive informational text inside the menu (no hover, not focusable). +.mcp-menu-info { @include t.use-typography("body-small"); padding: var(--sp-s) var(--sp-m); @@ -176,7 +167,7 @@ white-space: nowrap; } -.toolbar-mcp-dropdown-item { +.mcp-menu-item { @include t.use-typography("body-small"); display: flex; @@ -191,31 +182,26 @@ } } -.toolbar-separator { - --separator-color: var(--color-background-quaternary); - - margin: 0 var(--sp-m); - border-inline-start: $b-1 solid var(--separator-color); - block-size: $sz-32; -} - .toolbar-handler { - block-size: $sz-12; - display: flex; - justify-content: center; - align-items: center; - appearance: none; - border: none; - background: transparent; + @include deprecated.flex-center; + @include deprecated.button-style; + position: absolute; - inset-inline: 0; - inset-block-end: 0; + left: 0; + bottom: 0; + height: deprecated.$s-12; + width: 100%; + + .toolbar-handler-btn { + height: deprecated.$s-4; + width: 100%; + max-width: deprecated.$s-64; + padding: 0; + border-radius: deprecated.$s-4; + background-color: var(--palette-handler-background-color); + } } -.toolbar-handler-indicator { - block-size: px2rem(4); - inline-size: $sz-64; - padding: 0; - border-radius: $br-4; - background-color: var(--color-accent-default); +ul.main-toolbar-panels { + display: none; } diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index c3e9c466aa..3c5fb33114 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -53,9 +53,7 @@ [app.main.ui.workspace.viewport.selection :as selection] [app.main.ui.workspace.viewport.snap-distances :as snap-distances] [app.main.ui.workspace.viewport.snap-points :as snap-points] - [app.main.ui.workspace.viewport.top-bar :refer [grid-edition-bar* - path-edition-bar* - view-only-bar*]] + [app.main.ui.workspace.viewport.top-bar :refer [grid-edition-bar* path-edition-bar* view-only-bar*]] [app.main.ui.workspace.viewport.utils :as utils] [app.main.ui.workspace.viewport.viewport-ref :refer [create-viewport-ref]] [app.main.ui.workspace.viewport.widgets :as widgets] diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 6e41550ca7..45d2b5177e 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -51,9 +51,7 @@ [app.main.ui.workspace.viewport.selection :as selection] [app.main.ui.workspace.viewport.snap-distances :as snap-distances] [app.main.ui.workspace.viewport.snap-points :as snap-points] - [app.main.ui.workspace.viewport.top-bar :refer [grid-edition-bar* - path-edition-bar* - view-only-bar*]] + [app.main.ui.workspace.viewport.top-bar :refer [path-edition-bar* grid-edition-bar* view-only-bar*]] [app.main.ui.workspace.viewport.utils :as utils] [app.main.ui.workspace.viewport.viewport-ref :as vp-ref :refer [create-viewport-ref]] [app.main.ui.workspace.viewport.widgets :as widgets]