diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 79af070c6a..0d879ff02f 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -59,7 +59,9 @@ [:release-notes-viewed {:optional true} [::sm/text {:max 100}]] [:notifications {:optional true} schema:props-notifications] - [:workspace-visited {:optional true} ::sm/boolean]]) + [:workspace-visited {:optional true} ::sm/boolean] + [:custom-shortcuts {:optional true} + [:map-of {:gen/max 10} :keyword [:map-of :keyword :string]]]]) (def schema:profile [:map {:title "Profile"} diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 8190a4c1dc..d211648fe7 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -175,7 +175,10 @@ :mcp :background-blur :available-viewer-wasm - :stroke-per-side}) + :stroke-path + :stroke-per-side + + :custom-shortcuts}) (def all-flags (set/union email login varia)) diff --git a/frontend/playwright/data/logged-in-user/get-profile-no-shortcuts.json b/frontend/playwright/data/logged-in-user/get-profile-no-shortcuts.json new file mode 100644 index 0000000000..af49dca188 --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-profile-no-shortcuts.json @@ -0,0 +1,21 @@ +{ + "~:v2-info-shown": true, + "~:newsletter-updates": false, + "~:onboarding-viewed": true, + "~:onboarding-questions": { + "~:role": "ux", + "~:start-with": "wireframing", + "~:expected-use": "personal", + "~:experience-design-tool": "sketch" + }, + "~:onboarding-questions-answered": true, + "~:workspace-visited": true, + "~:renderer": "~:wasm", + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false, + "~:release-notes-viewed": "0.0", + "~:nudge": { + "~:big": 10, + "~:small": 1 + } +} diff --git a/frontend/playwright/data/logged-in-user/get-profile-with-shortcuts.json b/frontend/playwright/data/logged-in-user/get-profile-with-shortcuts.json new file mode 100644 index 0000000000..6fedc2bfae --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-profile-with-shortcuts.json @@ -0,0 +1,27 @@ +{ + "~:v2-info-shown": true, + "~:newsletter-updates": false, + "~:onboarding-viewed": true, + "~:onboarding-questions": { + "~:role": "ux", + "~:start-with": "wireframing", + "~:expected-use": "personal", + "~:experience-design-tool": "sketch" + }, + "~:onboarding-questions-answered": true, + "~:workspace-visited": true, + "~:custom-shortcuts": { + "~:workspace": { + "~:align-bottom": "ctrl+y", + "~:redo": "" + } + }, + "~:renderer": "~:wasm", + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false, + "~:release-notes-viewed": "0.0", + "~:nudge": { + "~:big": 10, + "~:small": 1 + } +} diff --git a/frontend/playwright/data/logged-in-user/update-profile-with-shortcuts.json b/frontend/playwright/data/logged-in-user/update-profile-with-shortcuts.json new file mode 100644 index 0000000000..6fedc2bfae --- /dev/null +++ b/frontend/playwright/data/logged-in-user/update-profile-with-shortcuts.json @@ -0,0 +1,27 @@ +{ + "~:v2-info-shown": true, + "~:newsletter-updates": false, + "~:onboarding-viewed": true, + "~:onboarding-questions": { + "~:role": "ux", + "~:start-with": "wireframing", + "~:expected-use": "personal", + "~:experience-design-tool": "sketch" + }, + "~:onboarding-questions-answered": true, + "~:workspace-visited": true, + "~:custom-shortcuts": { + "~:workspace": { + "~:align-bottom": "ctrl+y", + "~:redo": "" + } + }, + "~:renderer": "~:wasm", + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false, + "~:release-notes-viewed": "0.0", + "~:nudge": { + "~:big": 10, + "~:small": 1 + } +} diff --git a/frontend/playwright/ui/pages/ShortcutsPage.js b/frontend/playwright/ui/pages/ShortcutsPage.js new file mode 100644 index 0000000000..d434f9fb5b --- /dev/null +++ b/frontend/playwright/ui/pages/ShortcutsPage.js @@ -0,0 +1,328 @@ +import { expect } from "@playwright/test"; +import { readFile } from "node:fs/promises"; +import { BaseWebSocketPage } from "./BaseWebSocketPage"; + +function decodeKeyword(value) { + if (typeof value === "string" && value.startsWith("~:")) { + return value.slice(2); + } + if (typeof value === "string" && value.startsWith("~$")) { + return value.slice(2); + } + return value; +} + +function decodeTransit(data) { + if (Array.isArray(data)) { + if (data[0] === "^ ") { + const result = {}; + for (let i = 1; i < data.length; i += 2) { + const key = decodeTransit(data[i]); + const value = decodeTransit(data[i + 1]); + result[key] = value; + } + return result; + } + return data.map(decodeTransit); + } + + if (data !== null && typeof data === "object") { + const result = {}; + for (const [key, value] of Object.entries(data)) { + result[decodeKeyword(key)] = decodeTransit(value); + } + return result; + } + + return decodeKeyword(data); +} + +function encodeKeyword(key) { + return `~:${key}`; +} + +function encodeTransit(data) { + if (data === null || typeof data === "boolean" || typeof data === "number") { + return data; + } + + if (typeof data === "string") { + return data; + } + + if (Array.isArray(data)) { + return data.map(encodeTransit); + } + + const result = {}; + for (const [key, value] of Object.entries(data)) { + result[encodeKeyword(key)] = encodeTransit(value); + } + return result; +} + +function decodeRequestBody(body) { + try { + return decodeTransit(JSON.parse(body)); + } catch { + return null; + } +} + +export class ShortcutsPage extends BaseWebSocketPage { + static async init(page) { + await super.init(page); + + const profileText = await readFile( + "playwright/data/logged-in-user/get-profile-logged-in.json", + "utf-8", + ); + const baseProfile = decodeTransit(JSON.parse(profileText)); + let customShortcuts = null; + + await page.route("**/api/main/methods/get-profile", (route) => { + const profile = JSON.parse(JSON.stringify(baseProfile)); + if (customShortcuts) { + profile.props["custom-shortcuts"] = customShortcuts; + } + route.fulfill({ + status: 200, + contentType: "application/transit+json", + body: JSON.stringify(encodeTransit(profile)), + }); + }); + + await page.route( + "**/api/main/methods/update-profile-props", + async (route, request) => { + const decoded = decodeRequestBody(request.postData() ?? "{}"); + if (decoded?.props && "custom-shortcuts" in decoded.props) { + customShortcuts = decoded.props["custom-shortcuts"]; + } + route.fulfill({ + status: 200, + contentType: "application/transit+json", + body: "{}", + }); + }, + ); + + await super.mockRPC( + page, + "get-teams", + "logged-in-user/get-teams-default.json", + ); + } + + static async initWithShortcuts(page) { + await super.init(page); + + await super.mockRPCs(page, { + "get-profile": "logged-in-user/get-profile-logged-in.json", + "get-teams": "logged-in-user/get-teams-default.json", + "update-profile-props": + "logged-in-user/update-profile-with-shortcuts.json", + }); + } + + static async initWithNoShortcuts(page) { + await super.init(page); + + await super.mockRPCs(page, { + "get-profile": "logged-in-user/get-profile-no-shortcuts.json", + "get-teams": "logged-in-user/get-teams-default.json", + "update-profile-props": + "logged-in-user/update-profile-with-shortcuts.json", + }); + } + + constructor(page) { + super(page); + + this.shortcutsSection = page.getByRole("region", { name: /shortcuts/i }); + this.searchInput = page.getByRole("textbox", { name: /shortcuts/i }); + + this.allTab = page.getByRole("tab", { name: "All" }); + this.personalizedTab = page.getByRole("tab", { name: "Personalized" }); + this.disabledTab = page.getByRole("tab", { name: "Disabled" }); + + this.restoreAllButton = page.getByRole("button", { + name: /restore all/i, + }); + + this.importExportButton = page.getByRole("button", { + name: /import\/export/i, + }); + + this.fileInput = page.locator('input[type="file"]'); + } + + async goToShortcuts() { + await this.page.goto("#/settings/shortcuts"); + await expect(this.shortcutsSection).toBeVisible(); + } + + async searchForShortcut(term) { + await this.searchInput.fill(term); + } + + async clearSearch() { + await this.searchInput.clear(); + } + + async clickTab(tabName) { + const tab = this.page.getByRole("tab", { name: tabName }); + await tab.click(); + } + + async getShortcutRow(shortcutName) { + return this.page.getByRole("listitem", { + name: new RegExp(`^${shortcutName}$`, "i"), + includeHidden: true, + }); + } + + async expandSubsection(subsectionName) { + const subsectionButton = this.page.getByRole("button", { + name: subsectionName, + }); + await subsectionButton.click(); + } + + async clickEditShortcut(shortcutName) { + const button = this.page.getByRole("button", { + name: new RegExp(`Edit ${shortcutName}`, "i"), + }); + await button.click(); + const recordingArea = this.page.getByText("Press the key combination"); + await expect(recordingArea).toBeVisible(); + await recordingArea.focus(); + } + + async pressKey(key) { + await this.page.keyboard.press(key); + } + + async pressKeyCombo(keys) { + for (const key of keys) { + await this.page.keyboard.down(key); + } + for (const key of [...keys].reverse()) { + await this.page.keyboard.up(key); + } + } + + async saveShortcut() { + const saveButton = this.page.getByRole("button", { name: /save/i }); + await saveButton.click(); + } + + async cancelEdit() { + const cancelButton = this.page.getByRole("button", { name: /cancel/i }); + await cancelButton.click(); + } + + async resetShortcut(shortcutName) { + const row = await this.getShortcutRow(shortcutName); + const resetButton = row.getByRole("button", { name: /reset/i }); + await resetButton.click(); + } + + async disableShortcut(shortcutName) { + const row = await this.getShortcutRow(shortcutName); + await row + .getByRole("button", { name: new RegExp(`Edit ${shortcutName}`, "i") }) + .click(); + const disableButton = this.page.getByRole("button", { name: /disable/i }); + await disableButton.click(); + } + + async expectShortcutCustomized(shortcutName) { + const row = await this.getShortcutRow(shortcutName); + await expect(row).toHaveAttribute("data-customized", "true"); + } + + async expectShortcutNotCustomized(shortcutName) { + const row = await this.getShortcutRow(shortcutName); + await expect(row).not.toHaveAttribute("data-customized", "true"); + } + + async expectShortcutHasConflict(shortcutName) { + const row = await this.getShortcutRow(shortcutName); + await expect(row).toHaveAttribute("data-conflict", "true"); + } + + async expectShortcutVisible(shortcutName) { + const row = await this.getShortcutRow(shortcutName); + await expect(row).toBeVisible(); + } + + async expectShortcutHidden(shortcutName) { + const row = await this.getShortcutRow(shortcutName); + await expect(row).not.toBeVisible(); + } + + async restoreAllShortcuts() { + await this.restoreAllButton.click(); + const confirmButton = this.page.getByRole("button", { + name: "Restore", + exact: true, + }); + await confirmButton.click(); + } + + async exportShortcuts() { + await this.importExportButton.click(); + const exportButton = this.page.getByRole("menuitem", { name: /export/i }); + await exportButton.click(); + } + + async importShortcuts(jsonData) { + await this.importExportButton.click(); + const importButton = this.page.getByRole("menuitem", { name: /import$/i }); + await importButton.click(); + + const buffer = Buffer.from(JSON.stringify(jsonData), "utf-8"); + await this.fileInput.setInputFiles({ + name: "shortcuts.json", + mimeType: "application/json", + buffer, + }); + } + + async importShortcutsRaw(content) { + await this.importExportButton.click(); + const importButton = this.page.getByRole("menuitem", { name: /import$/i }); + await importButton.click(); + + const data = + typeof content === "string" ? content : JSON.stringify(content); + const buffer = Buffer.from(data, "utf-8"); + await this.fileInput.setInputFiles({ + name: "shortcuts.json", + mimeType: "application/json", + buffer, + }); + } + + async getExportedJson() { + const [download] = await Promise.all([ + this.page.waitForEvent("download"), + this.exportShortcuts(), + ]); + + expect(download.suggestedFilename()).toBe("penpot-shortcuts.json"); + + const path = await download.path(); + const content = await readFile(path, "utf-8"); + return JSON.parse(content); + } + + async expectShortcutDisabled(shortcutName) { + const row = await this.getShortcutRow(shortcutName); + await expect(row).toHaveAttribute("data-customized", "true"); + await expect(row.locator("use[href*='broken-link']")).toBeVisible(); + } +} + +export default ShortcutsPage; diff --git a/frontend/playwright/ui/specs/shortcuts.spec.js b/frontend/playwright/ui/specs/shortcuts.spec.js new file mode 100644 index 0000000000..afb8fbbd7f --- /dev/null +++ b/frontend/playwright/ui/specs/shortcuts.spec.js @@ -0,0 +1,323 @@ +import { test, expect } from "@playwright/test"; +import ShortcutsPage from "../pages/ShortcutsPage"; + +const customShortcutsFlag = "enable-custom-shortcuts"; + +test.beforeEach(async ({ page }) => { + await ShortcutsPage.init(page); + await ShortcutsPage.mockConfigFlags(page, [customShortcutsFlag]); +}); + +test.describe("Shortcuts Settings Page", () => { + test("Shortcuts page loads correctly", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await expect(shortcutsPage.shortcutsSection).toBeVisible(); + await expect(shortcutsPage.searchInput).toBeVisible(); + }); + + test("Tabs are visible and clickable", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await expect(shortcutsPage.allTab).toBeVisible(); + await expect(shortcutsPage.personalizedTab).toBeVisible(); + await expect(shortcutsPage.disabledTab).toBeVisible(); + + await shortcutsPage.clickTab("Personalized"); + await expect(shortcutsPage.personalizedTab).toHaveAttribute( + "aria-selected", + "true", + ); + const personalizedPlacehonder = page.getByText(/Head to All to start/i); + await expect(personalizedPlacehonder).toBeVisible(); + + await shortcutsPage.clickTab("Disabled"); + await expect(shortcutsPage.disabledTab).toHaveAttribute( + "aria-selected", + "true", + ); + const disabledPlaceholder = page.getByText(/There are not disabled/i); + await expect(disabledPlaceholder).toBeVisible(); + + await shortcutsPage.clickTab("All"); + await expect(shortcutsPage.allTab).toHaveAttribute("aria-selected", "true"); + }); +}); + +test.describe("Shortcut Customization", () => { + test("User can edit a shortcut", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + // Expand subsection + await shortcutsPage.expandSubsection("Alignment"); + + // Start edition of the shortcut + await shortcutsPage.clickEditShortcut("Align bottom"); + + // Press a new key combination + await shortcutsPage.pressKey("Control+y"); + + // Save the shortcut + await shortcutsPage.saveShortcut(); + + // Verify the shortcut is now customized + await shortcutsPage.expectShortcutCustomized("Align bottom"); + }); +}); + +test.describe("Shortcut Import", () => { + test("Import valid custom shortcuts", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.importShortcuts({ + workspace: { "align-bottom": "ctrl+y" }, + }); + + await shortcutsPage.expectShortcutCustomized("Align bottom"); + + await shortcutsPage.searchForShortcut("Align bottom"); + await shortcutsPage.clickTab("Personalized"); + await shortcutsPage.expectShortcutVisible("Align bottom"); + }); + + test("Import invalid JSON syntax", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.importShortcutsRaw("{invalid"); + + await expect( + page.getByRole("alert").filter({ hasText: /Invalid data/i }), + ).toBeVisible(); + }); + + test("Import valid JSON with invalid schema", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.importShortcuts({ + workspace: { "unknown-shortcut": "ctrl+y" }, + }); + + await expect( + page.getByRole("alert").filter({ hasText: /Invalid data/i }), + ).toBeVisible(); + }); + + test("Import JSON without workspace context accepted", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.importShortcuts({ + dashboard: { "toggle-theme": "alt+m" }, + }); + + await expect( + page.getByRole("alert").filter({ hasText: /Invalid data/i }), + ).not.toBeVisible(); + }); + + test("Import JSON with unsupported shortcut format", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.importShortcuts({ + workspace: { escape: 123 }, + }); + + await expect( + page.getByRole("alert").filter({ hasText: /Invalid data/i }), + ).toBeVisible(); + }); + + test("Import conflicting shortcuts", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.importShortcuts({ + workspace: { + "align-bottom": "alt+a", + }, + }); + + await shortcutsPage.expectShortcutCustomized("Align bottom"); + + await shortcutsPage.expandSubsection("Alignment"); + await shortcutsPage.expectShortcutDisabled("Align left"); + + const exported = await shortcutsPage.getExportedJson(); + expect(exported.workspace).toMatchObject({ + "align-bottom": "alt+a", + "align-left": "", + }); + }); +}); + +test.describe("Shortcut Export", () => { + test("Export customized shortcuts", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.expandSubsection("Alignment"); + await shortcutsPage.clickEditShortcut("Align bottom"); + await shortcutsPage.pressKey("Control+y"); + await shortcutsPage.saveShortcut(); + await shortcutsPage.expectShortcutCustomized("Align bottom"); + + const exported = await shortcutsPage.getExportedJson(); + expect(exported).toHaveProperty("workspace"); + expect(exported.workspace).toMatchObject({ + "align-bottom": "ctrl+y", + }); + }); + + test("Export includes disabled shortcuts", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.importShortcuts({ + workspace: { "align-bottom": "" }, + }); + + await shortcutsPage.searchForShortcut("Align bottom"); + await shortcutsPage.expectShortcutDisabled("Align bottom"); + + const exported = await shortcutsPage.getExportedJson(); + expect(exported.workspace).toMatchObject({ + "align-bottom": "", + }); + }); +}); + +test.describe("Shortcut Conflict Detection", () => { + test("Detects conflict and disables old shortcut on save", async ({ + page, + }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.expandSubsection("Alignment"); + await shortcutsPage.clickEditShortcut("Align bottom"); + await shortcutsPage.pressKey("Alt+a"); + + await expect( + page.getByRole("alert").filter({ hasText: /Combination assigned to/i }), + ).toBeVisible(); + + await shortcutsPage.saveShortcut(); + await shortcutsPage.expectShortcutCustomized("Align bottom"); + await shortcutsPage.expectShortcutDisabled("Align left"); + + const exported = await shortcutsPage.getExportedJson(); + expect(exported.workspace).toMatchObject({ + "align-bottom": "alt+a", + "align-left": "", + }); + }); +}); + +test.describe("Shortcut Reset", () => { + test("Reset after import restores defaults", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.importShortcuts({ + workspace: { "align-bottom": "ctrl+y" }, + }); + await shortcutsPage.expectShortcutCustomized("Align bottom"); + + await shortcutsPage.restoreAllShortcuts(); + await shortcutsPage.expectShortcutNotCustomized("Align bottom"); + }); +}); + +test.describe("Shortcut Persistence", () => { + test("Custom shortcuts persist after reload", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.expandSubsection("Alignment"); + await shortcutsPage.clickEditShortcut("Align bottom"); + await shortcutsPage.pressKey("Control+y"); + await shortcutsPage.saveShortcut(); + await shortcutsPage.expectShortcutCustomized("Align bottom"); + + await page.reload(); + await shortcutsPage.goToShortcuts(); + await shortcutsPage.expectShortcutCustomized("Align bottom"); + }); +}); + +test.describe("Cancel Shortcut Editing", () => { + test("Cancel editing does not save changes", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.expandSubsection("Alignment"); + await shortcutsPage.clickEditShortcut("Align bottom"); + await shortcutsPage.pressKey("Control+y"); + await shortcutsPage.cancelEdit(); + + await shortcutsPage.expectShortcutNotCustomized("Align bottom"); + }); +}); + +test.describe("Duplicate Shortcut Prevention", () => { + test("Assigning same shortcut to two actions shows conflict and disables first", async ({ + page, + }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.expandSubsection("Alignment"); + + await shortcutsPage.clickEditShortcut("Align bottom"); + await shortcutsPage.pressKey("Control+y"); + await shortcutsPage.saveShortcut(); + await shortcutsPage.expectShortcutCustomized("Align bottom"); + + await shortcutsPage.clickEditShortcut("Align left"); + await shortcutsPage.pressKey("Control+y"); + + await expect( + page.getByRole("alert").filter({ hasText: /Combination assigned to/i }), + ).toBeVisible(); + + await shortcutsPage.saveShortcut(); + await shortcutsPage.expectShortcutCustomized("Align left"); + + const exported = await shortcutsPage.getExportedJson(); + expect(exported.workspace).toMatchObject({ + "align-left": "ctrl+y", + "align-bottom": "", + }); + }); +}); + +test.describe("Shortcut Round-Trip", () => { + test("Export, reset, import restores configuration", async ({ page }) => { + const shortcutsPage = new ShortcutsPage(page); + await shortcutsPage.goToShortcuts(); + + await shortcutsPage.expandSubsection("Alignment"); + await shortcutsPage.clickEditShortcut("Align bottom"); + await shortcutsPage.pressKey("Control+y"); + await shortcutsPage.saveShortcut(); + await shortcutsPage.expectShortcutCustomized("Align bottom"); + + const exported = await shortcutsPage.getExportedJson(); + + await shortcutsPage.restoreAllShortcuts(); + await shortcutsPage.expectShortcutNotCustomized("Align bottom"); + + await shortcutsPage.importShortcuts(exported); + await shortcutsPage.expectShortcutCustomized("Align bottom"); + + const reExported = await shortcutsPage.getExportedJson(); + expect(reExported).toEqual(exported); + }); +}); diff --git a/frontend/src/app/main/data/dashboard/shortcuts.cljs b/frontend/src/app/main/data/dashboard/shortcuts.cljs index ccc0e87a64..2e737a660a 100644 --- a/frontend/src/app/main/data/dashboard/shortcuts.cljs +++ b/frontend/src/app/main/data/dashboard/shortcuts.cljs @@ -17,31 +17,36 @@ (def shortcuts {:toggle-theme {:tooltip (ds/alt "M") :command (ds/a-mod "m") - :subsections [:general-dashboard] + :section [:dashboard] + :subsections [:generic] :fn #(st/emit! (with-meta (du/toggle-theme) {::ev/origin "dashboard:shortcuts"}))}}) (def shortcuts-sidebar-navigation {:go-to-drafts {:tooltip "G D" :command "g d" + :section [:dashboard] :subsections [:navigation-dashboard] :fn #(st/emit! (dcm/go-to-dashboard-files :project-id :default))} :go-to-libs {:tooltip "G L" :command "g l" + :section [:dashboard] :subsections [:navigation-dashboard] :fn #(st/emit! (dcm/go-to-dashboard-libraries))}}) (def shortcut-search {:go-to-search {:tooltip (ds/meta "F") :command (ds/c-mod "f") + :section [:dashboard] :subsections [:navigation-dashboard] :fn #(st/emit! (dcm/go-to-dashboard-search))}}) (def shortcut-create-new-project {:create-new-project {:tooltip "+" :command "+" - :subsections [:general-dashboard] + :section [:dashboard] + :subsections [:generic] :fn #(st/emit! (dd/create-element))}}) ;; Shortcuts combinations for files, drafts, libraries and fonts sections diff --git a/frontend/src/app/main/data/dashboard/shortcuts/customize.cljs b/frontend/src/app/main/data/dashboard/shortcuts/customize.cljs new file mode 100644 index 0000000000..a9178efb1a --- /dev/null +++ b/frontend/src/app/main/data/dashboard/shortcuts/customize.cljs @@ -0,0 +1,77 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.data.dashboard.shortcuts.customize + (:require + [app.main.data.event :as ev] + [app.main.data.profile :as du] + [app.main.data.shortcuts :as ds] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) + +(defn set-custom-shortcut + [shortcut-key new-command conflicting-key group-key] + (ptk/reify ::set-custom-shortcut + ptk/WatchEvent + (watch [_ state _] + (let [current-customs (get-in state [:profile :props :custom-shortcuts] {}) + group-map (get current-customs group-key) + group-map (if (map? group-map) group-map {}) + group-map (assoc group-map shortcut-key new-command) + ;; clear conflicting shortcut in the same group + group-map (if conflicting-key + (assoc group-map conflicting-key "") + group-map) + new-customs (assoc current-customs group-key group-map)] + (rx/of + (ev/event {::ev/name "set-custom-shortcut"}) + (du/update-profile-props {:custom-shortcuts new-customs}) + (ds/rebind-shortcuts)))))) + + +(defn reset-custom-shortcut + [shortcut-key default-command group-key] + + (ptk/reify ::reset-custom-shortcut + ptk/WatchEvent + (watch [_ state _] + (let [current-customs (get-in state [:profile :props :custom-shortcuts] {}) + group-map (get current-customs group-key) + group-map (if (map? group-map) group-map {}) + + ;; Find any action that is currently using the default command as its custom shortcut + owner-key (when (and default-command (not= default-command "")) + (some (fn [[k v]] + (when (if (vector? default-command) + (some #{v} default-command) + (= v default-command)) + k)) + group-map)) + + ;; Remove the shortcut being reset + group-map (dissoc group-map shortcut-key) + + ;; Disable the owner that was using the default command + group-map (if (and owner-key (not= owner-key shortcut-key)) + (assoc group-map owner-key "") + group-map) + + new-customs (if (empty? group-map) + (dissoc current-customs group-key) + (assoc current-customs group-key group-map))] + (rx/of + (ev/event {::ev/name "reset-custom-shortcut"}) + (du/update-profile-props {:custom-shortcuts new-customs}) + (ds/rebind-shortcuts)))))) + +(defn reset-all-custom-shortcuts + [] + (ptk/reify ::reset-all-custom-shortcuts + ptk/WatchEvent + (watch [_ _ _] + (rx/of (ev/event {::ev/name "reset-all-custom-shortcuts"}) + (du/update-profile-props {:custom-shortcuts {}}) + (ds/rebind-shortcuts))))) diff --git a/frontend/src/app/main/data/shortcuts.cljs b/frontend/src/app/main/data/shortcuts.cljs index 7d263da514..8325370e45 100644 --- a/frontend/src/app/main/data/shortcuts.cljs +++ b/frontend/src/app/main/data/shortcuts.cljs @@ -121,6 +121,61 @@ [sc] (str/split sc #"\+| ")))) +(defn command->tooltip + "Converts a Mousetrap command string (e.g. \"command+shift+z\") to + a human-readable display string in the same format used by :tooltip + fields (e.g. \"⌘⇧Z\" on macOS or \"Ctrl+Shift+Z\" on Windows). + Returns nil for empty or unbound commands." + [command] + (when (and command (not= command "")) + (let [is-macos? (cf/check-platform? :macos) + parts (str/split command #"\+") + display-part (fn [p] + (case p + "ctrl" (if is-macos? mac-control "Ctrl+") + "command" (if is-macos? mac-command "Ctrl+") + "alt" (if is-macos? mac-option "Alt+") + "shift" (if is-macos? mac-shift "Shift+") + "up" up-arrow + "down" down-arrow + "left" left-arrow + "right" right-arrow + "del" (if is-macos? mac-delete "Del") + "backspace" (if is-macos? mac-delete "Backspace") + "escape" (if is-macos? mac-esc "Escape") + "enter" (if is-macos? mac-enter "Enter") + "space" "Space" + "tab" tab + (str/upper p)))] + (str/join "" (map display-part parts))))) + +(defn apply-custom-overrides + [shortcuts custom-overrides group-key] + (let [group-overrides (get custom-overrides group-key)] + (if (empty? group-overrides) + shortcuts + (reduce-kv + (fn [acc sc-key new-command] + (if (and (contains? acc sc-key) + (not (:disabled (get acc sc-key)))) + (-> acc + (assoc-in [sc-key :command] new-command) + (update sc-key dissoc :show-command)) + acc)) + shortcuts + group-overrides)))) + +(defn build-command-index + [shortcuts] + (reduce-kv + (fn [acc sc-key sc-def] + (let [commands (:command sc-def)] + (if (vector? commands) + (reduce #(assoc %1 %2 sc-key) acc commands) + (assoc acc commands sc-key)))) + {} + shortcuts)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Events ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -171,17 +226,34 @@ (fnil conj (d/ordered-map))) (defn push-shortcuts - [key shortcuts] + [key shortcuts group-key & {:keys [merge-shortcuts]}] (assert (keyword? key) "expected a keyword for `key`") (let [shortcuts (check-shortcuts shortcuts)] (ptk/reify ::push-shortcuts ptk/UpdateEvent (update [_ state] - (update state :shortcuts conj* [key shortcuts])) + (let [custom-overrides (get-in state [:profile :props :custom-shortcuts]) + group-key (or group-key (:shortcuts-group-key state)) + merge-sc (cond + (= merge-shortcuts :auto) + (let [[_ sc] (last (:shortcuts state))] + sc) + + (map? merge-shortcuts) + merge-shortcuts) + + effective (if merge-sc + (merge merge-sc shortcuts) + shortcuts) + effective (apply-custom-overrides effective custom-overrides group-key)] + (-> state + (update :shortcuts conj* [key effective]) + (assoc :shortcuts-group-key group-key)))) ptk/EffectEvent - (effect [_ _ _] - (reset! shortcuts))))) + (effect [_ state _] + (let [[_ stored] (last (:shortcuts state))] + (reset! stored)))))) (defn pop-shortcuts [key] @@ -193,5 +265,19 @@ ptk/EffectEvent (effect [_ state _] - (let [[_key shortcuts] (last (:shortcuts state))] - (reset! shortcuts))))) + (let [[_key shortcuts] (last (:shortcuts state)) + custom-overrides (get-in state [:profile :props :custom-shortcuts]) + group-key (:shortcuts-group-key state) + effective (apply-custom-overrides shortcuts custom-overrides group-key)] + (reset! effective))))) + +(defn rebind-shortcuts + [] + (ptk/reify ::rebind-shortcuts + ptk/EffectEvent + (effect [_ state _] + (let [custom-overrides (get-in state [:profile :props :custom-shortcuts]) + group-key (:shortcuts-group-key state) + merged (reduce merge (map val (:shortcuts state))) + effective (apply-custom-overrides merged custom-overrides group-key)] + (reset! effective))))) diff --git a/frontend/src/app/main/data/viewer/shortcuts.cljs b/frontend/src/app/main/data/viewer/shortcuts.cljs index 9d1cf39f46..f16dcb6d3a 100644 --- a/frontend/src/app/main/data/viewer/shortcuts.cljs +++ b/frontend/src/app/main/data/viewer/shortcuts.cljs @@ -7,6 +7,8 @@ (ns app.main.data.viewer.shortcuts (:require [app.main.data.common :as dcm] + [app.main.data.event :as ev] + [app.main.data.profile :as du] [app.main.data.shortcuts :as ds] [app.main.data.viewer :as dv] [app.main.store :as st])) @@ -15,61 +17,80 @@ {:increase-zoom {:tooltip "+" :command "+" :subsections [:zoom-viewer] + :section [:viewer] :fn #(st/emit! dv/increase-zoom)} + :toggle-theme {:tooltip (ds/alt "M") + :command (ds/a-mod "m") + :section [:viewer] + :subsections [:generic] + :fn #(st/emit! (with-meta (du/toggle-theme) + {::ev/origin "viewer:shortcuts"}))} + :decrease-zoom {:tooltip "-" :command "-" + :section [:viewer] :subsections [:zoom-viewer] :fn #(st/emit! dv/decrease-zoom)} :select-all {:tooltip (ds/meta "A") :command (ds/c-mod "a") - :subsections [:general-viewer] + :section [:viewer] + :subsections [:generic] :fn #(st/emit! (dv/select-all))} :reset-zoom {:tooltip (ds/shift "0") :command "shift+0" + :section [:viewer] :subsections [:zoom-viewer] :fn #(st/emit! dv/reset-zoom)} :toggle-zoom-style {:tooltip "F" :command "f" + :section [:viewer] :subsections [:zoom-viewer] :fn #(st/emit! dv/toggle-zoom-style)} :toggle-fullscreen {:tooltip (ds/shift "F") :command ["shift+f" "alt+enter"] + :section [:viewer] :subsections [:zoom-viewer] :fn #(st/emit! dv/toggle-fullscreen)} :prev-frame {:tooltip ds/left-arrow :command ["left" "up" "shift+enter" "pageup" "shift+space"] - :subsections [:general-viewer] + :subsections [:generic] + :section [:viewer] :fn #(st/emit! dv/select-prev-frame)} :next-frame {:tooltip ds/right-arrow :command ["right" "down" "enter" "pagedown" "space"] - :subsections [:general-viewer] + :subsections [:generic] + :section [:viewer] :fn #(st/emit! dv/select-next-frame)} :open-inspect {:tooltip "G I" :command "g i" :subsections [:navigation-viewer] + :section [:viewer] :fn #(st/emit! (dv/go-to-section :inspect))} :open-comments {:tooltip "G C" :command "g c" :subsections [:navigation-viewer] + :section [:viewer] :fn #(st/emit! (dv/go-to-section :comments))} :open-interactions {:tooltip "G V" :command "g v" :subsections [:navigation-viewer] + :section [:viewer] :fn #(st/emit! (dv/go-to-section :interactions))} :open-workspace {:tooltip "G W" :command "g w" :subsections [:navigation-viewer] + :section [:viewer] :fn #(st/emit! (dcm/go-to-workspace))}}) (defn get-tooltip [shortcut] diff --git a/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs b/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs index a6c468bc8a..e1ca153f48 100644 --- a/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs @@ -68,4 +68,4 @@ (defn get-tooltip [shortcut] (assert (contains? shortcuts shortcut) (str shortcut)) - (get-in shortcuts [shortcut :tooltip])) + (get-in shortcuts [shortcut :tooltip])) \ No newline at end of file diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 1db91e3eaa..a75e47cc6d 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -69,32 +69,38 @@ {;; EDIT :undo {:tooltip (ds/meta "Z") :command (ds/c-mod "z") + :section [:workspace] :subsections [:edit] :fn #(emit-when-no-readonly dwu/undo)} :redo {:tooltip (ds/meta "Y") :command [(ds/c-mod "shift+z") (ds/c-mod "y")] + :section [:workspace] :subsections [:edit] :fn #(emit-when-no-readonly dwu/redo)} :clear-undo {:tooltip (ds/alt "Q") :command "alt+q" :subsections [:edit] + :section [:workspace] :fn #(emit-when-no-readonly dwu/reinitialize-undo)} :copy {:tooltip (ds/meta "C") :command (ds/c-mod "c") :subsections [:edit] + :section [:workspace] :fn #(st/emit! (dw/copy-selected))} :copy-link {:tooltip (ds/shift (ds/alt "C")) :command "shift+alt+c" :subsections [:edit] + :section [:workspace] :fn #(st/emit! (dw/copy-link-to-clipboard))} :cut {:tooltip (ds/meta "X") :command (ds/c-mod "x") :subsections [:edit] + :section [:workspace] :fn #(emit-when-no-readonly (dw/copy-selected) (dw/delete-selected))} @@ -103,48 +109,57 @@ :disabled true :command (ds/c-mod "v") :subsections [:edit] + :section [:workspace] :fn (constantly nil)} :paste-replace {:tooltip (ds/meta (ds/shift "V")) :command (ds/c-mod "shift+v") :subsections [:edit] + :section [:workspace] :fn #(emit-when-no-readonly (dw/paste-from-clipboard {:replace? true}))} :copy-props {:tooltip (ds/meta (ds/alt "c")) :command (ds/c-mod "alt+c") :subsections [:edit] + :section [:workspace] :fn #(st/emit! (dw/copy-selected-props))} :paste-props {:tooltip (ds/meta (ds/alt "v")) :command (ds/c-mod "alt+v") :subsections [:edit] + :section [:workspace] :fn #(st/emit! (dw/paste-selected-props))} :delete {:tooltip (ds/supr) :command ["del" "backspace"] :subsections [:edit] + :section [:workspace] :fn #(emit-when-no-readonly (dw/delete-selected))} :duplicate {:tooltip (ds/meta "D") :command (ds/c-mod "d") :subsections [:edit] + :section [:workspace] :fn #(emit-when-no-readonly (dwv/duplicate-or-add-variant))} :start-editing {:tooltip (ds/enter) :command "enter" :subsections [:edit] + :section [:workspace] :fn #(emit-when-no-readonly (dw/start-editing-selected))} :start-measure {:tooltip (ds/alt "") :command ["alt" "."] :type "keydown" :subsections [:edit] + :section [:workspace] :fn #(emit-when-no-readonly (dw/toggle-distances-display true))} :stop-measure {:tooltip (ds/alt "") :command ["alt" "."] :type "keyup" :subsections [:edit] + :section [:workspace] :fn #(emit-when-no-readonly (dw/toggle-distances-display false))} :escape {:tooltip (ds/esc) @@ -153,8 +168,10 @@ :fn #(st/emit! :interrupt (dwdc/clear-drawing) (dw/deselect-all true))} :find {:tooltip (ds/meta "F") :command (ds/c-mod "f") :subsections [:edit] + :section [:workspace] :fn #(st/emit! (dw/open-layers-search :find))} :find-and-replace {:tooltip (ds/meta "H") :command (ds/c-mod "h") :subsections [:edit] + :section [:workspace] :fn #(st/emit! (dw/open-layers-search :find-and-replace))} ;; MODIFY LAYERS @@ -162,11 +179,13 @@ :rename {:tooltip (ds/alt "N") :command "alt+n" :subsections [:edit] + :section [:workspace] :fn #(emit-when-no-readonly (dw/start-rename-selected))} :group {:tooltip (ds/meta "G") :command (ds/c-mod "g") :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dw/group-selected))} :ungroup {:tooltip (ds/shift "G") @@ -177,99 +196,119 @@ :mask {:tooltip (ds/meta "M") :command (ds/c-mod "m") :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dw/mask-group))} :unmask {:tooltip (ds/meta-shift "M") :command (ds/c-mod "shift+m") :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dw/unmask-group))} :create-component-variant {:tooltip (ds/meta "K") :command (ds/c-mod "k") :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dwv/add-component-or-variant))} :detach-component {:tooltip (ds/meta-shift "K") :command (ds/c-mod "shift+k") :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly dwl/detach-selected-components)} :flip-vertical {:tooltip (ds/shift "V") :command "shift+v" :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dw/flip-vertical-selected))} :flip-horizontal {:tooltip (ds/shift "H") :command "shift+h" :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dw/flip-horizontal-selected))} :bring-forward {:tooltip (ds/meta ds/up-arrow) :command (ds/c-mod "up") :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dw/vertical-order-selected :up))} :bring-backward {:tooltip (ds/meta ds/down-arrow) :command (ds/c-mod "down") :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dw/vertical-order-selected :down))} :bring-front {:tooltip (ds/meta-shift ds/up-arrow) :command (ds/c-mod "shift+up") :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dw/vertical-order-selected :top))} :bring-back {:tooltip (ds/meta-shift ds/down-arrow) :command (ds/c-mod "shift+down") :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dw/vertical-order-selected :bottom))} :move-fast-up {:tooltip (ds/shift ds/up-arrow) :command ["shift+up" "shift+alt+up"] :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dwt/move-selected :up true))} :move-fast-down {:tooltip (ds/shift ds/down-arrow) :command ["shift+down" "shift+alt+down"] :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dwt/move-selected :down true))} :move-fast-right {:tooltip (ds/shift ds/right-arrow) :command ["shift+right" "shift+alt+right"] :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dwt/move-selected :right true))} :move-fast-left {:tooltip (ds/shift ds/left-arrow) :command ["shift+left" "shift+alt+left"] :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dwt/move-selected :left true))} :move-unit-up {:tooltip ds/up-arrow :command ["up" "alt+up"] :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dwt/move-selected :up false))} :move-unit-down {:tooltip ds/down-arrow :command ["down" "alt+down"] + :section [:workspace] :subsections [:modify-layers] :fn #(emit-when-no-readonly (dwt/move-selected :down false))} :move-unit-left {:tooltip ds/right-arrow :command ["right" "alt+right"] :subsections [:modify-layers] + :section [:workspace] :fn #(emit-when-no-readonly (dwt/move-selected :right false))} :move-unit-right {:tooltip ds/left-arrow :command ["left" "alt+left"] + :section [:workspace] :subsections [:modify-layers] :fn #(emit-when-no-readonly (dwt/move-selected :left false))} :artboard-selection {:tooltip (ds/meta (ds/alt "G")) :command (ds/c-mod "alt+g") + :section [:workspace] :subsections [:modify-layers] :fn #(emit-when-no-readonly (dws/create-artboard-from-selection))} :toggle-layout-flex {:tooltip (ds/shift "A") :command "shift+a" + :section [:workspace] :subsections [:modify-layers] :fn #(emit-when-no-readonly (with-meta (dwsl/toggle-layout :flex) @@ -277,6 +316,7 @@ :toggle-layout-grid {:tooltip (ds/meta-shift "A") :command (ds/c-mod "shift+a") + :section [:workspace] :subsections [:modify-layers] :fn #(emit-when-no-readonly (with-meta (dwsl/toggle-layout :grid) @@ -285,32 +325,38 @@ :draw-frame {:tooltip "B" :command ["b" "a"] + :section [:workspace :basics] :subsections [:tools :basics] :fn #(emit-when-no-readonly (dwd/select-for-drawing :frame))} :move {:tooltip "V" :command "v" + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly :interrupt)} :draw-rect {:tooltip "R" :command "r" + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly (dwd/select-for-drawing :rect))} :draw-ellipse {:tooltip "E" :command "e" + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly (dwd/select-for-drawing :circle))} :draw-text {:tooltip "T" :command "t" + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly dwtxt/start-edit-if-selected (dwd/select-for-drawing :text))} :draw-path {:tooltip "P" :command "p" + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly (dwd/select-for-drawing :path))} @@ -326,52 +372,62 @@ :draw-curve {:tooltip (ds/shift "C") :command "shift+c" + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly (dwd/select-for-drawing :curve))} :add-comment {:tooltip "C" :command "c" + :section [:workspace] :subsections [:tools] :fn #(st/emit! (dwd/select-for-drawing :comments))} :toggle-comments-visibility {:tooltip (ds/meta-shift "C") :command (ds/c-mod "shift+c") + :section [:workspace] :subsections [:main-menu] :fn #(st/emit! (dwcm/toggle-comments-visibility {:origin "workspace-shortcuts"}))} :insert-image {:tooltip (ds/shift "K") :command "shift+k" + :section [:workspace] :subsections [:tools] :fn #(-> "image-upload" dom/get-element dom/click)} :toggle-visibility {:tooltip (ds/meta-shift "H") :command (ds/c-mod "shift+h") + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly (dw/toggle-visibility-selected))} :toggle-lock {:tooltip (ds/meta-shift "L") :command (ds/c-mod "shift+l") + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly (dw/toggle-lock-selected))} :toggle-lock-size {:tooltip (ds/shift "L") :command "shift+l" + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly (dw/toggle-proportion-lock))} :scale {:tooltip "K" :command "k" + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly (toggle-layout-flag :scale-text))} :open-color-picker {:tooltip "I" :command "i" + :section [:workspace] :subsections [:tools] :fn #(emit-when-no-readonly (mdc/picker-for-selected-shape))} :toggle-focus-mode {:command "f" :tooltip "F" + :section [:workspace :basics] :subsections [:basics :tools] :fn #(st/emit! (dw/toggle-focus-mode))} @@ -379,41 +435,49 @@ :align-left {:tooltip (ds/alt "A") :command "alt+a" + :section [:workspace] :subsections [:alignment] :fn #(emit-when-no-readonly (dw/align-objects :hleft))} :align-right {:tooltip (ds/alt "D") :command "alt+d" + :section [:workspace] :subsections [:alignment] :fn #(emit-when-no-readonly (dw/align-objects :hright))} :align-top {:tooltip (ds/alt "W") :command "alt+w" + :section [:workspace] :subsections [:alignment] :fn #(emit-when-no-readonly (dw/align-objects :vtop))} :align-hcenter {:tooltip (ds/alt "H") :command "alt+h" + :section [:workspace] :subsections [:alignment] :fn #(emit-when-no-readonly (dw/align-objects :hcenter))} :align-vcenter {:tooltip (ds/alt "V") :command "alt+v" + :section [:workspace] :subsections [:alignment] :fn #(emit-when-no-readonly (dw/align-objects :vcenter))} :align-bottom {:tooltip (ds/alt "S") :command "alt+s" + :section [:workspace] :subsections [:alignment] :fn #(emit-when-no-readonly (dw/align-objects :vbottom))} :h-distribute {:tooltip (ds/meta-shift (ds/alt "H")) :command (ds/c-mod "shift+alt+h") + :section [:workspace] :subsections [:alignment] :fn #(emit-when-no-readonly (dw/distribute-objects :horizontal))} :v-distribute {:tooltip (ds/meta-shift (ds/alt "V")) :command (ds/c-mod "shift+alt+v") + :section [:workspace] :subsections [:alignment] :fn #(emit-when-no-readonly (dw/distribute-objects :vertical))} @@ -421,11 +485,13 @@ :toggle-rulers {:tooltip (ds/meta-shift "R") :command (ds/c-mod "shift+r") + :section [:workspace] :subsections [:main-menu] :fn #(st/emit! (toggle-layout-flag :rulers))} :select-all {:tooltip (ds/meta "A") :command (ds/c-mod "a") + :section [:workspace] :subsections [:main-menu] :fn #(st/emit! (dw/select-all))} @@ -434,37 +500,44 @@ :command [(ds/c-mod "'") (ds/c-mod "219")] :show-command (ds/c-mod "'") :subsections [:main-menu] + :section [:workspace] :fn #(st/emit! (toggle-layout-flag :display-guides))} :toggle-alignment {:tooltip (ds/meta "\\") :command (ds/c-mod "\\") :subsections [:main-menu] + :section [:workspace] :fn #(st/emit! (toggle-layout-flag :dynamic-alignment))} :thumbnail-set {:tooltip (ds/shift "T") :command "shift+t" :subsections [:main-menu] + :section [:workspace] :fn #(st/emit! (dw/toggle-file-thumbnail-selected))} :show-pixel-grid {:tooltip (ds/shift ",") :command "shift+," :subsections [:main-menu] + :section [:workspace] :fn #(st/emit! (toggle-layout-flag :show-pixel-grid))} :snap-pixel-grid {:command "," :tooltip "," :subsections [:main-menu] + :section [:workspace] :fn #(st/emit! (toggle-layout-flag :snap-pixel-grid))} :export-shapes {:tooltip (ds/meta-shift "E") :command (ds/c-mod "shift+e") :subsections [:basics :main-menu] + :section [:workspace :basics] :fn #(st/emit! (de/show-workspace-export-dialog {:origin "workspace:shortcuts"}))} :toggle-snap-ruler-guide {:tooltip (ds/meta-shift "G") :command (ds/c-mod "shift+g") :subsections [:main-menu] + :section [:workspace] :fn #(st/emit! (toggle-layout-flag :snap-ruler-guides))} :toggle-snap-guides {:tooltip (ds/meta-shift "'") @@ -472,11 +545,13 @@ :command [(ds/c-mod "shift+'") (ds/c-mod "shift+219")] :show-command (ds/c-mod "shift+'") :subsections [:main-menu] + :section [:workspace] :fn #(st/emit! (toggle-layout-flag :snap-guides))} :show-shortcuts {:tooltip "?" :command "?" :subsections [:main-menu] + :section [:workspace] :fn #(st/emit! (toggle-layout-flag :shortcuts))} ;; PANELS @@ -484,22 +559,26 @@ :toggle-layers {:tooltip (ds/alt "L") :command (ds/a-mod "l") :subsections [:panels] + :section [:workspace] :fn #(st/emit! (dcm/go-to-workspace :layout :layers))} :toggle-assets {:tooltip (ds/alt "I") :command (ds/a-mod "i") :subsections [:panels] + :section [:workspace] :fn #(st/emit! (dcm/go-to-workspace :layout :assets))} :toggle-history {:tooltip (ds/meta-alt "H") :command (ds/ca-mod "h") :subsections [:panels] + :section [:workspace] :fn #(emit-when-no-readonly (dw/toggle-layout-flag :document-history))} :toggle-colorpalette {:tooltip (ds/alt "P") :command (ds/a-mod "p") :subsections [:panels] + :section [:workspace] :fn #(do (r/set-resize-type! :bottom) (emit-when-no-readonly (dw/remove-layout-flag :hide-palettes) (dw/remove-layout-flag :textpalette) @@ -508,6 +587,7 @@ :toggle-textpalette {:tooltip (ds/alt "T") :command (ds/a-mod "t") :subsections [:panels] + :section [:workspace] :fn #(do (r/set-resize-type! :bottom) (emit-when-no-readonly (dw/remove-layout-flag :hide-palettes) (dw/remove-layout-flag :colorpalette) @@ -516,6 +596,7 @@ :hide-ui {:tooltip "\\" :command "\\" :subsections [:panels :basics] + :section [:workspace :basics] :fn #(st/emit! (toggle-layout-flag :hide-ui))} ;; ZOOM-WORKSPACE @@ -523,36 +604,43 @@ :increase-zoom {:tooltip "+" :command ["+" "="] :subsections [:zoom-workspace] + :section [:workspace] :fn #(st/emit! (dw/increase-zoom))} :decrease-zoom {:tooltip "-" :command ["-" "_"] :subsections [:zoom-workspace] + :section [:workspace] :fn #(st/emit! (dw/decrease-zoom))} :reset-zoom {:tooltip (ds/shift "0") :command ["shift+0" "shift+num0"] :subsections [:zoom-workspace] + :section [:workspace] :fn #(st/emit! dw/reset-zoom)} :fit-all {:tooltip (ds/shift "1") :command ["shift+1" "shift+num1"] :subsections [:zoom-workspace] + :section [:workspace] :fn #(st/emit! dw/zoom-to-fit-all)} :zoom-selected {:tooltip (ds/shift "2") :command ["shift+2" "shift+num2" "@" "\""] :subsections [:zoom-workspace] + :section [:workspace] :fn #(st/emit! dw/zoom-to-selected-shape)} :zoom-lense-increase {:tooltip "Z" :command "z" :subsections [:zoom-workspace] + :section [:workspace] :fn identity} :zoom-lense-decrease {:tooltip (ds/alt "Z") :command "alt+z" :subsections [:zoom-workspace] + :section [:workspace] :fn identity} ;; NAVIGATION @@ -561,36 +649,43 @@ :open-viewer {:tooltip "G V" :command "g v" :subsections [:navigation-workspace] + :section [:workspace] :fn #(st/emit! (dcm/go-to-viewer))} :open-inspect {:tooltip "G I" :command "g i" :subsections [:navigation-workspace] + :section [:workspace] :fn #(st/emit! (dcm/go-to-viewer :section :inspect))} :open-comments {:tooltip "G C" :command "g c" :subsections [:navigation-workspace] + :section [:workspace] :fn #(st/emit! (dcm/go-to-viewer :section :comments))} :open-dashboard {:tooltip "G D" :command "g d" :subsections [:navigation-workspace] + :section [:workspace] :fn #(st/emit! (dcm/go-to-dashboard-recent))} :select-prev {:tooltip (ds/shift "tab") :command "shift+tab" :subsections [:navigation-workspace] + :section [:workspace] :fn #(st/emit! (dw/select-prev-shape))} :select-next {:tooltip ds/tab :command "tab" :subsections [:navigation-workspace] + :section [:workspace] :fn #(st/emit! (dw/select-next-shape))} :select-parent-layer {:tooltip (ds/shift ds/enter) :command "shift+enter" :subsections [:navigation-workspace] + :section [:workspace] :fn #(emit-when-no-readonly (dw/select-parent-layer))} ;; SHAPE @@ -598,27 +693,32 @@ :bool-union {:tooltip (ds/meta (ds/alt "U")) :command (ds/c-mod "alt+u") :subsections [:shape] + :section [:workspace] :fn #(emit-when-no-readonly (dw/create-bool :union))} :bool-difference {:tooltip (ds/meta (ds/alt "D")) :command (ds/c-mod "alt+d") :subsections [:shape] + :section [:workspace] :fn #(emit-when-no-readonly (dw/create-bool :difference))} :bool-intersection {:tooltip (ds/meta (ds/alt "I")) :command (ds/c-mod "alt+i") :subsections [:shape] + :section [:workspace] :fn #(emit-when-no-readonly (dw/create-bool :intersection))} :bool-exclude {:tooltip (ds/meta (ds/alt "E")) :command (ds/c-mod "alt+e") :subsections [:shape] + :section [:workspace] :fn #(emit-when-no-readonly (dw/create-bool :exclude))} ;; THEME :toggle-theme {:tooltip (ds/alt "M") :command (ds/a-mod "m") - :subsections [:basics] + :subsections [:generic :basics] + :section [:workspace :basics] :fn #(st/emit! (with-meta (du/toggle-theme) {::ev/origin "workspace:shortcut"}))} @@ -626,7 +726,8 @@ ;; PLUGINS :plugins {:tooltip (ds/meta (ds/alt "P")) :command (ds/c-mod "alt+p") - :subsections [:basics] + :subsections [:generic :basics] + :section [:workspace :basics] :fn #(when (features/active-feature? @st/state "plugins/runtime") (st/emit! (ev/event {::ev/name "open-plugins-manager" ::ev/origin "workspace:shortcuts"}) @@ -644,6 +745,7 @@ (map (fn [n] [(keyword (str "opacity-" n)) {:tooltip (str n) :command [(str n) (str "num" n)] + :section [:workspace] :subsections [:modify-layers] :fn #(emit-when-no-readonly (dwly/pressed-opacity n))}]))))) @@ -652,6 +754,24 @@ *assert* (merge debug-shortcuts))) -(defn get-tooltip [shortcut] +(defn get-tooltip + "Returns the tooltip string for a shortcut, using any custom binding + from the user's profile props if one exists, falling back to the + default :tooltip field." + [shortcut] (assert (contains? shortcuts shortcut) (str shortcut)) - (get-in shortcuts [shortcut :tooltip])) + (let [custom-shortcuts (deref refs/custom-shortcuts) + custom-command (get-in custom-shortcuts [:workspace shortcut])] + (if (and custom-command (not= custom-command "")) + (ds/command->tooltip custom-command) + (get-in shortcuts [shortcut :tooltip])))) + +(defn get-effective-tooltip + "Returns the tooltip string for a shortcut given an already-resolved + custom-shortcuts map. Use this when you already have the custom + shortcuts derefed for the current render cycle." + [shortcut custom-shortcuts] + (let [custom-command (get-in custom-shortcuts [:workspace shortcut])] + (if (and custom-command (not= custom-command "")) + (ds/command->tooltip custom-command) + (get-in shortcuts [shortcut :tooltip])))) diff --git a/frontend/src/app/main/data/workspace/text/shortcuts.cljs b/frontend/src/app/main/data/workspace/text/shortcuts.cljs index c1e050ad6b..fddfaf357a 100644 --- a/frontend/src/app/main/data/workspace/text/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/text/shortcuts.cljs @@ -237,31 +237,37 @@ {:underline {:tooltip (ds/meta "U") :command (ds/c-mod "u") :subsections [:text-editor] + :section [:workspace] :fn #(update-attrs-when-no-readonly {:text-decoration "toggle-underline"})} :line-through {:tooltip (ds/alt (ds/meta-shift "5")) :command "alt+shift+5" :subsections [:text-editor] + :section [:workspace] :fn #(update-attrs-when-no-readonly {:text-decoration "toggle-line-through"})} :font-size-inc {:tooltip (ds/meta-shift ">") :command (ds/c-mod "shift+.") :subsections [:text-editor] + :section [:workspace] :fn #(update-attrs-when-no-readonly {:font-size-inc true})} :font-size-dec {:tooltip (ds/meta-shift "<") :command (ds/c-mod "shift+,") :subsections [:text-editor] + :section [:workspace] :fn #(update-attrs-when-no-readonly {:font-size-dec true})} :bold {:tooltip (ds/meta "b") :command (ds/c-mod "b") :subsections [:text-editor] + :section [:workspace] :fn #(update-attrs-when-no-readonly {:font-variant-id "toggle-bold"})} :italic {:tooltip (ds/meta "i") :command (ds/c-mod "i") :subsections [:text-editor] + :section [:workspace] :fn #(update-attrs-when-no-readonly {:font-variant-id "toggle-italic"})}}) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 08d0a3ab7a..a73ae9b872 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -32,6 +32,9 @@ (def profile (l/derived (l/key :profile) st/state)) +(def custom-shortcuts + (l/derived (fn [state] (get-in state [:profile :props :custom-shortcuts])) st/state)) + (def current-page-id (l/derived (l/key :current-page-id) st/state)) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 20a3f500d4..0cb9f5b68c 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -203,7 +203,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/components/search_bar.scss b/frontend/src/app/main/ui/components/search_bar.scss index 075e176405..59f37812b2 100644 --- a/frontend/src/app/main/ui/components/search_bar.scss +++ b/frontend/src/app/main/ui/components/search_bar.scss @@ -10,7 +10,7 @@ display: flex; gap: deprecated.$s-2; height: deprecated.$s-32; - width: 100%; + flex-grow: 1; border-radius: deprecated.$br-8; background-color: var(--search-bar-background-color); } diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index 1986876c9f..0ba58aedb6 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -33,6 +33,8 @@ (def is-render? (mf/create-context false)) (def is-component? (mf/create-context false)) +(def shortcuts-ctx (mf/create-context nil)) + (def sidebar "A context that intends to store the current sidebar position, useful for components that behaves distinctly if they are showed in diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index fb96681b6c..781bb1ec49 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -305,7 +305,7 @@ (filter :is-default) (first)))] - (hooks/use-shortcuts ::dashboard sc/shortcuts-dashboard) + (hooks/use-shortcuts ::dashboard sc/shortcuts-dashboard :dashboard) (mf/with-effect [team-id] (st/emit! (dd/initialize team-id)) diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 05ed77851e..ed3781faba 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -195,7 +195,7 @@ (st/emit! (dpj/fetch-files project-id) (dd/clear-selected-files))) - (hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries) + (hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries :dashboard) [:* [:> header* {:team team diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index c674dfe868..f907d04b53 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -60,7 +60,7 @@ (st/emit! (dtm/fetch-shared-files team-id) (dd/clear-selected-files))) - (hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries) + (hooks/use-shortcuts ::dashboard sc/shortcuts-drafts-libraries :dashboard) [:* [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 56e84ad53c..542395caa7 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -369,7 +369,7 @@ (st/emit! (dd/fetch-recent-files team-id) (dd/clear-selected-files))) - (hooks/use-shortcuts ::dashboard sc/shortcuts-projects) + (hooks/use-shortcuts ::dashboard sc/shortcuts-projects :dashboard) (when (seq projects) [:* diff --git a/frontend/src/app/main/ui/ds/buttons/button.cljs b/frontend/src/app/main/ui/ds/buttons/button.cljs index 35e99dea00..73b6a65180 100644 --- a/frontend/src/app/main/ui/ds/buttons/button.cljs +++ b/frontend/src/app/main/ui/ds/buttons/button.cljs @@ -42,4 +42,4 @@ (on-ref node)))})] [:> element props (when icon [:> icon* {:icon-id icon :size "m"}]) - [:span {:class (stl/css :label-wrapper)} children]])) + children])) diff --git a/frontend/src/app/main/ui/ds/product/empty_state.cljs b/frontend/src/app/main/ui/ds/product/empty_state.cljs index 9b738cb98d..3dd6ef739f 100644 --- a/frontend/src/app/main/ui/ds/product/empty_state.cljs +++ b/frontend/src/app/main/ui/ds/product/empty_state.cljs @@ -14,7 +14,7 @@ (def ^:private schema:empty-state [:map [:class {:optional true} :string] - [:icon [:and :string [:fn #(contains? icon-list %)]]] + [:icon {:optional true} [:and :string [:fn #(contains? icon-list %)]]] [:text :string]]) (mf/defc empty-state* @@ -22,8 +22,9 @@ [{:keys [class icon text] :rest props}] (let [props (mf/spread-props props {:class [class (stl/css :group)]})] [:> :div props - [:div {:class (stl/css :icon-wrapper)} - [:> icon* {:icon-id icon - :size "l" - :class (stl/css :icon)}]] + (when icon + [:div {:class (stl/css :icon-wrapper)} + [:> icon* {:icon-id icon + :size "l" + :class (stl/css :icon)}]]) [:div {:class (stl/css :text)} text]])) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index ed13c36e4e..1c7478868c 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -42,13 +42,14 @@ state)) (defn use-shortcuts - [key shortcuts] - (mf/use-effect - #js [(str key) shortcuts] - (fn [] - (st/emit! (dsc/push-shortcuts key shortcuts)) + [key shortcuts group-key] + (let [custom-shortcuts (mf/deref refs/custom-shortcuts)] + (mf/use-effect + #js [(str key) shortcuts custom-shortcuts] (fn [] - (st/emit! (dsc/pop-shortcuts key)))))) + (st/emit! (dsc/push-shortcuts key shortcuts group-key)) + (fn [] + (st/emit! (dsc/pop-shortcuts key))))))) (defn- set-timer [state ms func] diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index b33c3b6782..6d14ef811c 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..5ffa8b3bea 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]] @@ -38,7 +39,7 @@ (let [section (get-in route [:data :name]) profile (mf/deref refs/profile)] - (hooks/use-shortcuts ::dashboard sc/shortcuts) + (hooks/use-shortcuts ::dashboard sc/shortcuts :dashboard) (mf/with-effect [profile] (when (nil? profile) @@ -54,7 +55,7 @@ [:div {:class (stl/css :dashboard-content)} [:> header*] - [:section {:class (stl/css :dashboard-container)} + [:div {:class (stl/css :dashboard-container)} (case section :settings-profile [:> profile-page*] @@ -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/feedback.cljs b/frontend/src/app/main/ui/settings/feedback.cljs index 6b7e10fc0e..ee7fd553e9 100644 --- a/frontend/src/app/main/ui/settings/feedback.cljs +++ b/frontend/src/app/main/ui/settings/feedback.cljs @@ -98,7 +98,8 @@ :form form} ;; --- Feedback section - [:h2 {:class (stl/css :field-title :feedback-title)} (tr "feedback.title-contact-us")] + [:h2 {:class (stl/css :field-title :feedback-title) + :id "feedback-section-title"} (tr "feedback.title-contact-us")] [:p {:class (stl/css :field-text :feedback-title)} (tr "feedback.subtitle")] [:div {:class (stl/css :fields-row)} @@ -164,6 +165,7 @@ (:content errors/last-report)) props (mf/spread-props props {:error-report report})] - [:div {:class (stl/css :dashboard-settings)} + [:section {:class (stl/css :dashboard-settings) + :aria-labelledby "feedback-section-title"} [:div {:class (stl/css :form-container)} [:> feedback-form* props]]])) diff --git a/frontend/src/app/main/ui/settings/integrations.cljs b/frontend/src/app/main/ui/settings/integrations.cljs index 74867b3cc4..d8c137561a 100644 --- a/frontend/src/app/main/ui/settings/integrations.cljs +++ b/frontend/src/app/main/ui/settings/integrations.cljs @@ -640,10 +640,12 @@ (dom/set-html-title (tr "title.settings.integrations")) (st/emit! (du/fetch-access-tokens))) - [:div {:class (stl/css :integrations)} - [:> heading* {:level 1 + [:section {:class (stl/css :integrations) + :aria-labelledby "integrations-section-title"} + [:> heading* {:level 2 :typography t/title-large - :class (stl/css :color-primary)} + :class (stl/css :color-primary) + :id "integrations-section-title"} (tr "integrations.title")] (when ^boolean mcp-enabled? diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index aca2ef130c..23932ba133 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -122,10 +122,15 @@ (mf/use-effect #(dom/set-html-title (tr "title.settings.options"))) - [:div {:class (stl/css :dashboard-settings)} + [:section {:class (stl/css :dashboard-settings) + :aria-labelledby "options-section-title"} [:* [:div {:class (stl/css :form-container) :data-testid "settings-form"} - [:h2 (tr "labels.settings")] + [:> heading* {:level 2 + :typography t/title-large + :class (stl/css :color-primary) + :id "options-section-title"} + (tr "labels.settings")] [:> options-form*]] (when (contains? cf/flags :render-switch) [:> webgl-settings* {:renderer renderer}])]])) diff --git a/frontend/src/app/main/ui/settings/password.cljs b/frontend/src/app/main/ui/settings/password.cljs index 651b97c1a7..cf3d60ed8d 100644 --- a/frontend/src/app/main/ui/settings/password.cljs +++ b/frontend/src/app/main/ui/settings/password.cljs @@ -106,7 +106,8 @@ (mf/with-effect [] (dom/set-html-title (tr "title.settings.password"))) - [:section {:class (stl/css :dashboard-settings)} + [:section {:class (stl/css :dashboard-settings) + :aria-labelledby "password-section-title"} [:div {:class (stl/css :form-container)} - [:h2 (tr "dashboard.password-change")] + [:h2 {:id "password-section-title"} (tr "dashboard.password-change")] [:> password-form*]]]) diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index 124597a4e1..1fd133d794 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -143,8 +143,9 @@ (mf/with-effect [] (dom/set-html-title (tr "title.settings.profile"))) - [:div {:class (stl/css :dashboard-settings)} + [:section {:class (stl/css :dashboard-settings) + :aria-labelledby "profile-section-title"} [:div {:class (stl/css :form-container)} - [:h2 (tr "labels.profile")] + [:h2 {:id "profile-section-title"} (tr "labels.profile")] [:> profile-photo-form*] [:> profile-form*]]]) diff --git a/frontend/src/app/main/ui/settings/restore_shortcuts_modal.cljs b/frontend/src/app/main/ui/settings/restore_shortcuts_modal.cljs new file mode 100644 index 0000000000..a24091f7b2 --- /dev/null +++ b/frontend/src/app/main/ui/settings/restore_shortcuts_modal.cljs @@ -0,0 +1,166 @@ +(ns app.main.ui.settings.restore-shortcuts-modal + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.i18n :refer [tr]] + [app.main.data.dashboard.shortcuts :as dsc] + [app.main.data.dashboard.shortcuts.customize :as customize] + [app.main.data.modal :as modal] + [app.main.data.shortcuts :as ds] + [app.main.data.viewer.shortcuts :as vsc] + [app.main.data.workspace.path.shortcuts :as psc] + [app.main.data.workspace.shortcuts :as wsc] + [app.main.store :as st] + [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.shortcuts :as ss] + [app.util.dom :as dom] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(def ^:private workspace-shortcuts-raw + (d/deep-merge psc/shortcuts + wsc/shortcuts)) + +(def ^:private context->defaults + {:workspace workspace-shortcuts-raw + :dashboard dsc/shortcuts + :viewer vsc/shortcuts}) + +(defn extract-shortcut-keys [shortcut-key custom-shortcuts context] + (let [defaults (get context->defaults context) + default-command (:command (get defaults shortcut-key)) + + default-managed-list (if (coll? default-command) + default-command + (conj () default-command)) + + default-chars-list (map ds/split-sc default-managed-list) + + default-last-element (last default-chars-list) + + default-short-char-list (if (= 1 (count default-chars-list)) + default-chars-list + (drop-last default-chars-list)) + + ctx-customs (get custom-shortcuts context {}) + current-command (or (get ctx-customs shortcut-key) default-command) + + current-managed-list (if (coll? current-command) + current-command + (conj () current-command)) + + current-chars-list (map ds/split-sc current-managed-list) + + current-last-element (last current-chars-list) + + current-short-char-list (if (= 1 (count current-chars-list)) + current-chars-list + (drop-last current-chars-list))] + [default-last-element default-short-char-list current-last-element current-command current-short-char-list])) + +(mf/defc restore-all-modal + {::mf/register modal/components + ::mf/register-as :restore-all-modal} + [{:keys [custom-shortcuts]}] + (let [handle-close-dialog (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (st/emit! (modal/hide)))) + + handle-accept-dialog (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (st/emit! (customize/reset-all-custom-shortcuts)) + (st/emit! (modal/hide))))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog)} + [:> icon-button* {:class (stl/css :close-btn) + :variant "ghost" + :aria-label (tr "labels.close") + :on-click handle-close-dialog + :tooltip-class (stl/css :close-btn-tooltip) + :icon i/close}] + [:div {:class (stl/css :modal-title)} + (tr "restore-shortcuts.modal-title")] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :modal-content-text)} + (tr "restore-shortcuts.modal-text")] + + [:table {:class (stl/css :shortcuts-table)} + [:thead + [:tr {:class (stl/css :shortcuts-list-header)} + [:th {:class (stl/css :shortcut-header-name)} + (tr "restore-shortcuts.acction")] + [:th {:class (stl/css :shortcut-header-command)} + (tr "labels.current")] + [:th {:class (stl/css :shortcut-header-command)} + (tr "labels.default")]]] + [:tbody {:class (stl/css :shortcuts-list-body)} + (for [context [:workspace :dashboard :viewer] + shortcut-key (keys (get custom-shortcuts context {}))] + (let [[default-last-element + default-short-char-list + current-last-element + current-command + current-short-char-list] (extract-shortcut-keys shortcut-key custom-shortcuts context) + current-penultimate (last current-short-char-list) + default-penultimate (last default-short-char-list)] + [:tr {:key (dm/str (name context) "-" (name shortcut-key)) + :class (stl/css :shortcuts-list-item)} + [:td {:class (stl/css :shortcut-name)} + (ss/translation-keyname :sc shortcut-key)] + [:td {:class (stl/css :shortcut-command)} + (if (str/blank? current-command) + [:span {:class (stl/css :shortcut-empty)} "-"] + (for [chars current-short-char-list] + [:* {:key (str/join chars)} + (for [char chars] + [:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key)) + :char char + :class (stl/css :default-command) + :command shortcut-key}]) + (when (not= chars current-penultimate) [:span {:class (stl/css :space)} ","])])) + (when (not= current-last-element current-penultimate) + [:* + [:span {:class (stl/css :space)} (tr "shortcuts.or")] + (for [char current-last-element] + [:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key)) + :char char + :class (stl/css :default-command) + :command shortcut-key}])])] + + [:td {:class (stl/css :shortcut-command)} + (for [chars default-short-char-list] + [:* {:key (str/join chars)} + (for [char chars] + [:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key)) + :class (stl/css :default-command) + :char char + :command shortcut-key}]) + (when (not= chars default-penultimate) [:span {:class (stl/css :space)} ","])]) + (when (not= default-last-element default-penultimate) + [:* + [:span {:class (stl/css :space)} (tr "shortcuts.or")] + (for [char default-last-element] + [:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key)) + :class (stl/css :default-command) + :char char + :command shortcut-key}])])]]))]]] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:> button* {:class (stl/css :cancel-button) + :variant "secondary" + :type "button" + :on-click modal/hide!} + (tr "labels.cancel")] + [:> button* {:class (stl/css :cancel-button) + :type "button" + :variant "primary" + :on-click handle-accept-dialog} + (tr "restore-shortcuts.restore")]]]]])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/settings/restore_shortcuts_modal.scss b/frontend/src/app/main/ui/settings/restore_shortcuts_modal.scss new file mode 100644 index 0000000000..95da5b5147 --- /dev/null +++ b/frontend/src/app/main/ui/settings/restore_shortcuts_modal.scss @@ -0,0 +1,147 @@ +// 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 "ds/typography.scss" as t; +@use "ds/_borders.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; + +.modal-overlay { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + inset-inline-start: 0; + inset-block-start: 0; + block-size: 100%; + inline-size: 100%; + z-index: var(--z-index-set); + background-color: var(--overlay-color); +} + +.modal-dialog { + position: relative; + padding: var(--sp-xxxl); + border-radius: $br-8; + background-color: var(--color-background-primary); + border: $b-2 solid var(--color-background-quaternary); + display: grid; + grid-template-rows: auto 1fr auto; + gap: var(--sp-xxxl); + min-inline-size: $sz-364; + min-block-size: $sz-192; + max-block-size: $sz-520; + inline-size: $sz-500; + max-inline-size: $sz-712; +} + +.modal-title { + @include t.use-typography("headline-medium"); + + color: var(--color-foreground-primary); + block-size: px2rem(26); +} + +.close-btn { + position: absolute; + inset-block-start: px2rem(8); + inset-inline-end: px2rem(6); + block-size: $sz-32; + inline-size: $sz-28; +} + +.close-btn-tooltip { + position: absolute; + inset-block-start: px2rem(8); + inset-inline-end: px2rem(6); +} + +.modal-content { + display: flex; + flex-direction: column; + gap: var(--sp-l); + block-size: fit-content; +} + +.modal-content-text { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-primary); +} + +.modal-footer { + justify-self: end; + display: flex; + flex-direction: row; + gap: var(--sp-s); +} + +.action-buttons { + display: flex; + justify-content: flex-end; + gap: var(--sp-s); +} + +.shortcuts-table { + inline-size: 100%; + table-layout: fixed; + border-collapse: collapse; +} + +.shortcuts-list-item, +.shortcuts-list-header { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-secondary); + display: grid; + grid-template-columns: 40% 30% 30%; +} + +.shortcuts-list-header { + border-block-end: $b-1 solid var(--color-foreground-secondary); + block-size: $sz-32; + text-align: start; + padding-block-end: var(--sp-s); +} + +.shortcuts-list-body { + display: flex; + flex-direction: column; + gap: var(--sp-s); +} + +.shortcuts-list-item { + color: var(--color-foreground-primary); +} + +.shortcuts-list-item:first-child { + padding-block-start: var(--sp-s); +} + +.shortcut-header-name, +.shortcut-header-command { + text-align: start; +} + +.shortcut-command { + display: flex; + gap: var(--sp-s); + align-items: center; + justify-content: start; + text-align: start; + flex-wrap: wrap; +} + +.default-command { + background-color: var(--color-background-tertiary); + color: var(--color-foreground-secondary); + border-radius: $br-6; +} + +.shortcut-empty { + color: var(--color-foreground-secondary); +} 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..24b49cba1d --- /dev/null +++ b/frontend/src/app/main/ui/settings/shortcuts.cljs @@ -0,0 +1,479 @@ +(ns app.main.ui.settings.shortcuts + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.i18n :refer [tr]] + [app.common.json :as json] + [app.common.schema :as sm] + [app.main.data.dashboard.shortcuts :as dsc] + [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] + [app.main.data.shortcuts :as ds] + [app.main.data.viewer.shortcuts :as vsc] + [app.main.data.workspace.path.shortcuts :as psc] + [app.main.data.workspace.shortcuts :as wsc] + [app.main.store :as st] + [app.main.ui.components.dropdown-menu :refer [dropdown-menu* + dropdown-menu-item*]] + [app.main.ui.components.search-bar :refer [search-bar*]] + [app.main.ui.context :as ctx] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [app.main.ui.ds.foundations.typography :as t] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.foundations.typography.text :refer [text*]] + [app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]] + [app.main.ui.ds.product.empty-state :refer [empty-state*]] + [app.main.ui.settings.restore-shortcuts-modal] + [app.main.ui.shortcuts :as ss] + [app.util.dom :as dom] + [app.util.strings :refer [matches-search]] + [app.util.webapi :as wapi] + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [malli.error :as me] + [rumext.v2 :as mf])) + +(mf/defc search-section* + [{:keys [filter-term on-search-term-change on-search-clear-click on-restore-all show-restore-all? has-custom-shortcuts]}] + [:div {:class (stl/css :shortcuts-search-section)} + [:> search-bar* {:on-change on-search-term-change + :on-clear on-search-clear-click + :value filter-term + :placeholder (tr "shortcuts.title") + :icon-id i/search + :auto-focus true}] + (when (and show-restore-all? has-custom-shortcuts) + [:> button* + {:variant "secondary" + :on-click on-restore-all + :class (stl/css :restore-all-button) + :icon i/reload} + (tr "dashboard.restore-all-deleted-button")])]) + +(defn- filter-shortcuts-tree + [tree shortcut-filter search-term] + (let [sorted-comp (when (sorted? tree) + #(compare (name %1) (name %2))) + result (keep (fn [[k node]] + (let [children (:children node)] + (cond + (map? children) + (let [filtered-children + (filter-shortcuts-tree children shortcut-filter search-term)] + (when (seq filtered-children) + [k (assoc node :children filtered-children)])) + + (shortcut-filter k node search-term) + [k node]))) + tree)] + (if sorted-comp + (into (sorted-map-by sorted-comp) result) + (into {} result)))) + +(defn- collect-open-section-ids + [tree] + (letfn [(walk [nodes] + (mapcat + (fn [node] + (let [children (:children node)] + (concat + (when (:children node) + [(:id node)]) + (when (map? children) + (walk (vals children)))))) + nodes))] + (->> (walk (vals tree)) + (remove nil?) + set))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Imported shortcuts JSON validation +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private known-shortcut-keys + "Known shortcut keys per context, derived from the default shortcuts maps." + {:workspace (set (keys wsc/shortcuts)) + :dashboard (set (keys dsc/shortcuts)) + :viewer (set (keys vsc/shortcuts))}) + +(def ^:private schema:imported-shortcuts + "Malli schema for an imported custom-shortcuts payload. + + The expected shape is a map of shortcut contexts to maps of shortcut-key -> command-string. + Only keys that exist in the default shortcuts for each context are accepted." + (letfn [(context-schema [ctx] + (into [:map {:closed true}] + (map (fn [k] [k {:optional true} :string])) + (get known-shortcut-keys ctx)))] + [:map {:closed true} + [:workspace {:optional true} (context-schema :workspace)] + [:dashboard {:optional true} (context-schema :dashboard)] + [:viewer {:optional true} (context-schema :viewer)]])) + +(defn- flatten-humanize + "Turns Malli's humanized explain output into a flat list of {:path :message} maps." + ([human] (flatten-humanize [] human)) + ([prefix human] + (cond + (or (string? human) (symbol? human) (keyword? human)) + [{:path (str/join "/" (map #(if (keyword? %) (name %) (str %)) prefix)) + :message (str human)}] + + (map? human) + (mapcat (fn [[k v]] (flatten-humanize (conj prefix k) v)) human) + + (sequential? human) + (mapcat #(flatten-humanize prefix %) human) + + :else []))) + +(defn validate-imported-shortcuts + "Validates an imported custom-shortcuts JSON payload. + + Returns {:valid? true} when the payload conforms to the expected shape, or + {:valid? false :errors [{:path \"...\" :message \"...\"} ...]} otherwise. + Never throws for validation errors." + [data] + (try + (if-not (map? data) + {:valid? false + :errors [{:path "" + :message "Expected a map of shortcut contexts"}]} + (let [explain (sm/explain schema:imported-shortcuts data)] + (if (nil? explain) + {:valid? true} + {:valid? false + :errors (-> explain me/humanize flatten-humanize vec)}))) + (catch :default e + {:valid? false + :errors [{:path "" + :message (str "Validation failed: " (.-message e))}]}))) + +(mf/defc shortcuts-list* + [{:keys [shortcuts open-sections filter-term custom-shortcuts editable? manage-sections conflicts]}] + (let [sections (mf/with-memo [shortcuts] + (keep (fn [section] + (when (seq (get-in section [1 :children])) + section)) + shortcuts))] + [:div {:class (stl/css :shortcuts-list) + :aria-label (tr "shortcuts.title")} + (for [section sections] + (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? editable? + :custom-shortcuts custom-shortcuts + :conflicts conflicts}]))])) + +(mf/defc shortcuts-tab-section* + [{:keys [shortcut-filter show-restore-all? on-restore-all empty-str custom-shortcuts conflicts]}] + (let [{:keys [all-shortcuts]} (mf/use-ctx ctx/shortcuts-ctx) + open-sections* (mf/use-state [[:workspace]]) + open-sections (deref open-sections*) + + filter-term* (mf/use-state "") + filter-term (deref filter-term*) + + 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)) + filtered-shortcuts (filter-shortcuts-tree all-shortcuts shortcut-filter filter-term) + + on-search-term-change + (mf/use-fn + (mf/deps all-shortcuts shortcut-filter) + (fn [term] + (reset! filter-term* term) + (if (str/blank? term) + (reset! open-sections* [[:workspace]]) + (let [filtered-tree (filter-shortcuts-tree all-shortcuts shortcut-filter term) + open-ids (collect-open-section-ids filtered-tree)] + (reset! open-sections* open-ids))))) + + on-search-clear-click + (mf/use-fn + (fn [_] + (reset! open-sections* [[:workspace]]) + (reset! filter-term* ""))) + + 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))))] + + [:div {:class (stl/css :shortcuts-section)} + [:> search-section* {:filter-term filter-term + :on-search-term-change on-search-term-change + :on-search-clear-click on-search-clear-click + :on-restore-all on-restore-all + :show-restore-all? show-restore-all? + :has-custom-shortcuts (some #(seq (val %)) custom-shortcuts)}] + (if (seq filtered-shortcuts) + [:> shortcuts-list* {:shortcuts filtered-shortcuts + :open-sections open-sections + :filter-term filter-term* + :custom-shortcuts custom-shortcuts + :editable? true + :manage-sections manage-sections + :conflicts conflicts}] + + [:> empty-state* {:text empty-str + :class (stl/css :shortcuts-empty-state)}])])) + +(mf/defc shortcuts-page* + [{:keys [profile]}] + (let [section* (mf/use-state :all) + section (deref section*) + + show-menu* (mf/use-state false) + show-menu? (deref show-menu*) + + open-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! show-menu* true))) + + close-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! show-menu* false))) + + input-ref (mf/use-ref nil) + + tabs + (mf/with-memo [] + [{:label (tr "labels.all") + :id "all"} + {:label (tr "shortcuts.personalized") + :data-testid "personalized" + :id "personalized"} + {:label (tr "shortcuts.disabled") + :data-testid "disabled" + :id "disabled"}]) + + handle-change-tab + (mf/use-fn + (fn [new-section] + (reset! section* (keyword new-section)))) + + custom-shortcuts (get-in profile [:props :custom-shortcuts]) + has-custom-shortcuts (some #(seq (val %)) custom-shortcuts) + path-shortcuts-custom (ds/apply-custom-overrides + psc/shortcuts + custom-shortcuts + :workspace) + path-shortcuts-with-translation (->> path-shortcuts-custom + (ss/add-translation :sc) + (into {})) + workspace-shortcuts-custom (ds/apply-custom-overrides + wsc/shortcuts + custom-shortcuts + :workspace) + workspace-shortcuts-with-translation (->> workspace-shortcuts-custom + (ss/add-translation :sc) + (into {})) + dashboard-shortcuts-custom (ds/apply-custom-overrides + dsc/shortcuts + custom-shortcuts :dashboard) + dashboard-shortcuts-with-translation (->> dashboard-shortcuts-custom + (ss/add-translation :sc) + (into {})) + viewer-shortcuts-custom (ds/apply-custom-overrides + vsc/shortcuts + custom-shortcuts + :viewer) + + viewer-shortcuts-with-translation (->> viewer-shortcuts-custom + (ss/add-translation :sc) + (into {})) + + all-shortcuts-raw (d/deep-merge + workspace-shortcuts-custom + path-shortcuts-custom + dashboard-shortcuts-custom + viewer-shortcuts-custom) + + {:keys [all-shortcuts]} + (ss/build-all-shortcuts-without-basics + workspace-shortcuts-with-translation + path-shortcuts-with-translation + dashboard-shortcuts-with-translation + viewer-shortcuts-with-translation + :workspace-orig wsc/shortcuts + :path-orig psc/shortcuts + :dashboard-orig dsc/shortcuts + :viewer-orig vsc/shortcuts) + + shortcuts-ctx-value + {:workspace-sc-trans (d/deep-merge workspace-shortcuts-with-translation path-shortcuts-with-translation) + :dashboard-sc-trans dashboard-shortcuts-with-translation + :viewer-sc-trans viewer-shortcuts-with-translation + :all-shortcuts all-shortcuts + :all-sc-raw all-shortcuts-raw} + + on-restore-all + (mf/use-fn + (mf/deps custom-shortcuts) + (fn [event] + (dom/stop-propagation event) + (st/emit! (modal/show {:type :restore-all-modal + :custom-shortcuts custom-shortcuts})))) + + on-import-file + (mf/use-fn + (fn [_] + (dom/click (mf/ref-val input-ref)))) + + shortcuts-json + (mf/with-memo + [custom-shortcuts] + (some-> custom-shortcuts + (json/encode :key-fn d/name :indent 2))) + + on-export + (mf/use-fn + (mf/deps shortcuts-json has-custom-shortcuts) + (fn [] + (when has-custom-shortcuts + (->> (wapi/create-blob shortcuts-json "application/json") + (dom/trigger-download "penpot-shortcuts.json"))))) + + on-file-selected + (mf/use-fn + (mf/deps all-shortcuts-raw) + (fn [event] + (let [file (-> (dom/get-target event) + (dom/get-files) + (first))] + (->> (wapi/read-file-as-text file) + (rx/subs! + (fn [content] + (try + (let [shortcuts (js->clj (.parse js/JSON content) + :keywordize-keys true) + validation (validate-imported-shortcuts shortcuts)] + (if (:valid? validation) + (st/emit! (ss/import-custom-shortcuts shortcuts all-shortcuts-raw)) + (st/emit! (ntf/error (tr "errors.invalid-data"))))) + (catch :default _ + (st/emit! (ntf/error (tr "errors.invalid-data")))))))) + + (-> (mf/ref-val input-ref) + (dom/set-value! "")))))] + + [:section {:class (stl/css :shortcuts-page) + :aria-label (tr "shortcuts.page")} + [:div {:class (stl/css :shortcuts-content)} + [:> heading* {:level 2 + :typography t/title-large + :class (stl/css :page-title)} + (tr "label.shortcuts")] + [:> text* {:class (stl/css :shortcuts-description) + :as "span" + :typography t/body-small} + [:> icon* {:icon-id i/info + :class (stl/css :shortcuts-description-icon)}] + (tr "shortcuts.reload-hint")] + + [:> (mf/provider ctx/shortcuts-ctx) {:value shortcuts-ctx-value} + [:div {:class (stl/css :shortcuts-content)} + [:> tab-switcher* {:tabs tabs + :selected (name section) + :on-change handle-change-tab + :class (stl/css :shortcuts-switcher)} + (case section + :all + [:> shortcuts-tab-section* {:shortcut-filter (fn [_ shortcut search-term] + (or (str/blank? search-term) + (matches-search (:translation shortcut) search-term))) + :show-restore-all? true + :empty-str (tr "shortcuts.no-shortcuts") + :on-restore-all on-restore-all + :custom-shortcuts custom-shortcuts}] + + :personalized + [:> shortcuts-tab-section* {:shortcut-filter (fn [shortcut-key shortcut search-term] + (let [shortcut-group (first (:section shortcut)) + group-map (get custom-shortcuts shortcut-group) + group-map (if (map? group-map) group-map {}) + customized? (and (contains? group-map shortcut-key) + (not (str/blank? (get group-map shortcut-key))))] + (and customized? + (or (str/blank? search-term) + (matches-search (:translation shortcut) search-term))))) + :show-restore-all? true + :empty-str (tr "shortcuts.no-personalized") + :on-restore-all on-restore-all + :custom-shortcuts custom-shortcuts}] + + :disabled + [:> shortcuts-tab-section* {:shortcut-filter (fn [shortcut-key shortcut search-term] + (let [shortcut-group (first (:section shortcut)) + group-map (get custom-shortcuts shortcut-group) + group-map (if (map? group-map) group-map {}) + in-group? (contains? group-map shortcut-key) + blank? (str/blank? (get group-map shortcut-key))] + (and in-group? blank? + (or (str/blank? search-term) + (matches-search (:translation shortcut) search-term))))) + :show-restore-all? true + :empty-str (tr "shortcuts.no-disabled") + :on-restore-all on-restore-all + :custom-shortcuts custom-shortcuts}])]] + + [:div {:class (stl/css :shortcuts-page-footer)} + [:div {:class (stl/css :shortcuts-info)} + [:div {:class (stl/css :shortcuts-info-wrapper)} + [:div {:class (stl/css :dot-wrapper)} + [:div {:class (stl/css :shortcuts-customized-dot)}]] + [:p {:class (stl/css :shortcuts-text)} + (tr "shortcuts.personalized")]] + + [:div {:class (stl/css :shortcuts-info-wrapper)} + [:> icon* {:class (stl/css :shortcuts-not-assigned-icon) + :icon-id i/broken-link}] + [:p {:class (stl/css :shortcuts-text)} + (tr "shortcuts.disabled")]]] + + [:div {:class (stl/css :export-wrapper)} + + [:> button* {:variant "secondary" + :on-click open-menu + :icon i/import-export} + (tr "shortcuts.import-export")] + [:> dropdown-menu* {:show show-menu? + :on-close close-menu + :id "tokens-menu" + :class (stl/css :import-export-menu)} + [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) + :on-click on-import-file} + [:div {:class (stl/css :import-menu-item)} + [:div (tr "labels.import")]]] + (when has-custom-shortcuts + [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) + :on-click on-export} + (tr "labels.export")])]] + + [:input + {:type "file" + :accept ".json,application/json" + :ref input-ref + :style {:display "none"} + :on-change on-file-selected}]]]]])) 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..e9395e2fe5 --- /dev/null +++ b/frontend/src/app/main/ui/settings/shortcuts.scss @@ -0,0 +1,324 @@ +// 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 "ds/typography.scss" as t; +@use "ds/_borders.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/z-index.scss" as *; + +.shortcuts-page { + display: flex; + justify-content: center; + padding-block-end: px2rem(32); +} + +.shortcuts-list { + @include t.use-typography("body-small"); + + display: flex; + flex-direction: column; + block-size: 100%; + padding: var(--sp-m); + overflow-y: auto; + color: var(--color-foreground-secondary); + min-block-size: px2rem(250); +} + +.shortcuts-content { + display: flex; + flex-direction: column; + max-inline-size: px2rem(590); + inline-size: 100%; +} + +.page-title { + color: var(--color-foreground-primary); + inline-size: 100%; + margin-block-start: px2rem(80); +} + +.shortcuts-section { + margin-block-end: var(--sp-lg); +} + +.shortcuts-search-section { + display: flex; + justify-content: center; + gap: var(--sp-xs); + padding-block-start: var(--sp-s); +} + +.restore-all-button { + min-inline-size: px2rem(130); + max-inline-size: px2rem(300); + inline-size: fit-content; +} + +// RESTORE ALL MODAL + +.modal-overlay { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + inset-inline-start: 0; + inset-block-start: 0; + block-size: 100%; + inline-size: 100%; + z-index: var(--z-index-set); + background-color: var(--overlay-color); +} + +.modal-dialog { + position: relative; + padding: var(--sp-xxxl); + border-radius: $br-8; + background-color: var(--color-background-primary); + border: $b-2 solid var(--color-background-quaternary); + display: grid; + grid-template-rows: 0 auto 1fr; + gap: var(--sp-xxxl); + min-inline-size: $sz-364; + min-block-size: $sz-192; + max-block-size: $sz-520; + inline-size: $sz-500; + max-inline-size: $sz-712; +} + +.modal-title { + @include t.use-typography("headline-medium"); + + color: var(--color-foreground-primary); + block-size: px2rem(26); +} + +.close-btn { + position: absolute; + inset-block-start: px2rem(8); + inset-inline-end: px2rem(6); + block-size: $sz-32; + inline-size: $sz-28; +} + +.modal-content { + display: flex; + flex-direction: column; + gap: var(--sp-l); + block-size: fit-content; +} + +.modal-content-text { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-primary); +} + +.modal-footer { + justify-self: end; + display: flex; + flex-direction: row; + gap: var(--sp-s); +} + +.action-buttons { + display: flex; + justify-content: flex-end; + gap: var(--sp-s); +} + +.shortcuts-table { + inline-size: 100%; + table-layout: fixed; + border-collapse: collapse; +} + +.shortcuts-list-item, +.shortcuts-list-header { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-secondary); + display: grid; + grid-template-columns: 40% 30% 30%; +} + +.shortcuts-list-header { + border-block-end: $b-1 solid var(--color-foreground-secondary); + block-size: $sz-32; + text-align: start; + padding-block-end: var(--sp-s); +} + +.shortcuts-list-body { + display: flex; + flex-direction: column; + gap: var(--sp-s); +} + +.shortcuts-list-item { + color: var(--color-foreground-primary); +} + +.shortcuts-list-item:first-child { + padding-block-start: var(--sp-s); +} + +.shortcut-header-name, +.shortcut-header-command { + text-align: start; +} + +.shortcut-command { + display: flex; + gap: var(--sp-s); + align-items: center; + justify-content: start; + text-align: start; + flex-wrap: wrap; +} + +.default-command { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); + background-color: var(--color-background-tertiary); + padding: px2rem(6) var(--sp-xs); +} + +.shortcuts-page-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sp-s); + padding-block-start: var(--sp-m); +} + +.shortcuts-info { + display: flex; + gap: var(--sp-s); + align-items: center; +} + +.shortcuts-page-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sp-s); + padding-block-start: var(--sp-m); +} + +.shortcuts-info { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); +} + +.shortcuts-customized-dot, +.shortcuts-conflict-dot { + inline-size: px2rem(4); + block-size: px2rem(4); + border-radius: 50%; + background-color: var(--color-accent-info); +} + +.shortcuts-conflict-dot { + background-color: var(--color-accent-error); +} + +.shortcuts-info { + display: flex; + gap: var(--sp-m); +} + +.shortcuts-info-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: var(--sp-xxs); +} + +.dot-wrapper { + display: flex; + align-items: center; + justify-content: center; + block-size: px2rem(16); + inline-size: px2rem(16); +} + +.shortcuts-text { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); + margin: 0; +} + +.shortcuts-not-assigned-icon { + margin-inline-end: var(--sp-xs); +} + +.export-wrapper { + position: relative; +} + +.import-export-menu { + box-shadow: 0 0 px2rem(12) 0 var(--color-shadow-dark); + display: flex; + flex-direction: column; + gap: var(--sp-xs); + position: absolute; + padding: var(--sp-xs); + border-radius: $br-8; + z-index: var(--z-index-dropdown); + color: var(--color-foreground-primary); + background-color: var(--color-background-tertiary); + border: $b-2 solid var(--color-background-quaternary); + inset-block-start: calc(-1 * px2rem(6)); + inset-inline-end: 0; + translate: 0 -100%; + inline-size: px2rem(192); + margin: 0; +} + +.import-export-menu-item { + @include t.use-typography("body-medium"); + + display: flex; + align-items: center; + justify-content: space-between; + block-size: $sz-28; + inline-size: 100%; + padding: px2rem(6); + border-radius: $br-8; + cursor: pointer; + + &:hover { + background-color: var(--color-background-quaternary); + color: var(--color-foreground-primary); + } +} + +.import-export-menu-item-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.shortcuts-empty-state { + display: flex; + justify-content: center; + align-items: center; + block-size: px2rem(250); +} + +.shortcuts-description { + color: var(--color-foreground-secondary); + display: flex; + align-items: center; + gap: var(--sp-xs); + padding: var(--sp-s) 0; + margin-block-end: var(--sp-l); +} diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index 94c634004f..3ddff8f862 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)) @@ -83,7 +87,8 @@ [:hr {:class (stl/css :sidebar-separator)}] - [:div {:class (stl/css :sidebar-content-section)} + [:nav {:class (stl/css :sidebar-content-section) + :aria-label (tr "labels.settings")} [:ul {:class (stl/css :sidebar-nav-settings)} [:li {:class (stl/css-case :current profile? :settings-item true) @@ -100,6 +105,12 @@ :on-click go-settings-notifications} [:span {:class (stl/css :element-title)} (tr "labels.notifications")]] + (when (contains? cf/flags :custom-shortcuts) + [:li {:class (stl/css-case :current shortcuts? + :settings-item true) + :on-click go-settings-shortcuts} + [:span {:class (stl/css :element-title)} (tr "label.shortcuts")]]) + [:li {:class (stl/css-case :current options? :settings-item true) :on-click go-settings-options @@ -137,7 +148,7 @@ (mf/defc sidebar* {::mf/wrap [mf/memo]} [{:keys [profile section]}] - [:div {:class (stl/css :dashboard-sidebar :settings)} + [:aside {:class (stl/css :dashboard-sidebar :settings)} [:> sidebar-content* {:profile profile :section section}] [:> profile-section* {:profile profile}]]) diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index bdd82bb890..e405beb8b8 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -565,10 +565,10 @@ (tr "subscription.settings.enterprise")))})) (rt/nav :settings-subscription {} {::rt/replace true}))))) - [:section {:class (stl/css :dashboard-section)} + [:section {:class (stl/css :dashboard-section) + :aria-labelledby "subscription-section-title"} [:div {:class (stl/css :dashboard-content)} - [:h2 {:class (stl/css :title-section)} (tr "subscription.labels")] - + [:h2 {:id "subscription-section-title" :class (stl/css :title-section)} (tr "subscription.labels")] [:div {:class (stl/css :your-subscription)} [:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")] diff --git a/frontend/src/app/main/ui/shortcuts.cljs b/frontend/src/app/main/ui/shortcuts.cljs new file mode 100644 index 0000000000..2a27c2742b --- /dev/null +++ b/frontend/src/app/main/ui/shortcuts.cljs @@ -0,0 +1,700 @@ +;; 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.dashboard.shortcuts.customize :as customize] + [app.main.data.profile :as du] + [app.main.data.shortcuts :as ds] + [app.main.store :as st] + [app.main.ui.context :as ctx] + [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.notifications.context-notification :refer [context-notification*]] + [app.main.ui.ds.tooltip.tooltip :refer [tooltip*]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [app.util.strings :refer [matches-search]] + [cuerdas.core :as str] + [goog.events :as events] + [rumext.v2 :as mf])) + +(def ^:private modifier-keys + #{"Control" "Shift" "Alt" "Meta"}) + +(def ^:private key-name-map + {"ArrowUp" "up" + "ArrowDown" "down" + "ArrowLeft" "left" + "ArrowRight" "right" + "Escape" "escape" + "Enter" "enter" + "Backspace" "backspace" + "Delete" "del" + "Tab" "tab" + " " "space"}) + +(defn- keyboard-event->mousetrap + [^js event] + (let [parts (cond-> [] + (and (.-ctrlKey event) + (not (cf/check-platform? :macos))) + (conj "ctrl") + + (and (.-metaKey event) + (cf/check-platform? :macos)) + (conj "command") + + (.-altKey event) + (conj "alt") + + (.-shiftKey event) + (conj "shift")) + key (.-key event) + key (if (contains? modifier-keys key) + nil + (or (get key-name-map key) + (.toLowerCase key)))] + (when key + (str/join "+" (conj parts key))))) + +(defn- keyboard-event->display-parts + [^js event] + (let [parts (cond-> [] + (and (.-ctrlKey event) + (not (cf/check-platform? :macos))) + (conj "ctrl") + + (and (.-metaKey event) + (cf/check-platform? :macos)) + (conj "command") + + (.-altKey event) + (conj "alt") + + (.-shiftKey event) + (conj "shift")) + key (.-key event)] + (if (contains? modifier-keys key) + {:modifiers parts :finalized? false} + {:modifiers parts + :final-key (or (get key-name-map key) (.toLowerCase key)) + :finalized? true}))) + +(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- find-conflict + [new-command all-shortcuts current-key] + (let [command-index (ds/build-command-index all-shortcuts)] + (when-let [conflicting-key (get command-index new-command)] + (when (not= conflicting-key current-key) + {:key conflicting-key + :name (translation-keyname :sc conflicting-key)})))) + +(def ^:private import-contexts + [:workspace :dashboard :viewer]) + +(defn- import-context-group + "Imports a single context group from the payload, disabling any default + shortcut whose command collides with a newly imported one, and any + previously-imported entry in the same batch with a duplicate command." + [group all-shortcuts] + (reduce + (fn [acc [command recorded-command]] + (let [default-conflict (find-conflict recorded-command all-shortcuts command) + acc-conflict (some (fn [[k v]] + (when (and (not= k command) (= v recorded-command)) + k)) + acc)] + (cond-> (assoc acc command recorded-command) + (:key default-conflict) + (assoc (:key default-conflict) "") + acc-conflict + (assoc acc-conflict "")))) + {} + group)) + +(defn import-custom-shortcuts + [shortcuts all-shortcuts] + (let [current-customs (or (get-in shortcuts [:profile :props :custom-shortcuts]) + (get-in @st/state [:profile :props :custom-shortcuts]) + {}) + + new-customs (reduce + (fn [acc ctx] + (if (contains? shortcuts ctx) + (assoc acc ctx (import-context-group (get shortcuts ctx) all-shortcuts)) + acc)) + current-customs + import-contexts)] + + (du/update-profile-props {:custom-shortcuts new-customs}))) + +(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)) + subsections (vec (sort-by name subsections)) + 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 (sorted-map-by (fn [a b] (compare (name a) (name b)))) subsections))) + +(defn build-all-shortcuts + [workspace-sc dashboard-sc viewer-sc] + (let [walk (fn walk [element parent-id] + (if (nil? element) + element + (let [sorted-comp (when (sorted? element) + #(compare (name %1) (name %2))) + rec-fn (fn [[k item]] + (let [item-id (if (nil? parent-id) + [k] + (conj parent-id k))] + [k (assoc item :id item-id :children (walk (:children item) item-id))]))] + (if sorted-comp + (into (sorted-map-by sorted-comp) (map rec-fn element)) + (into {} (map rec-fn element)))))) + sorted-map-fn #(sorted-map-by (fn [a b] (compare (name a) (name b)))) + ws-subs (->> (shortcuts->subsections workspace-sc) + (add-translation :sub-sec) + (into (sorted-map-fn))) + + db-subs (->> (shortcuts->subsections dashboard-sc) + (add-translation :sub-sec) + (into (sorted-map-fn))) + vw-subs (->> (shortcuts->subsections viewer-sc) + (add-translation :sub-sec) + (into (sorted-map-fn))) + 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 {:children {:none {:children basics}} + :translation (tr "shortcut-section.basics")} + :workspace {:children ws-subs + :translation (tr "shortcut-section.workspace")} + :dashboard {:children db-subs + :translation (tr "shortcut-section.dashboard")} + :viewer {: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})) + +(defn build-all-shortcuts-without-basics + [workspace-sc path-sc dashboard-sc viewer-sc + & {:keys [workspace-orig path-orig dashboard-orig viewer-orig]}] + (let [original-commands (into {} + (concat + (map (fn [[k v]] [k (:command v)]) workspace-orig) + (map (fn [[k v]] [k (:command v)]) path-orig) + (map (fn [[k v]] [k (:command v)]) dashboard-orig) + (map (fn [[k v]] [k (:command v)]) viewer-orig))) + walk (fn walk [element parent-id] + (if (nil? element) + element + (let [sorted-comp (when (sorted? element) + #(compare (name %1) (name %2))) + rec-fn (fn [[k item]] + (let [item-id (if (nil? parent-id) + [k] + (conj parent-id k)) + item (if (:command item) + (assoc item :original-command (get original-commands k (:command item))) + item)] + [k (assoc item :id item-id :children (walk (:children item) item-id))]))] + (if sorted-comp + (into (sorted-map-by sorted-comp) (map rec-fn element)) + (into {} (map rec-fn element)))))) + sorted-map-fn #(sorted-map-by (fn [a b] (compare (name a) (name b)))) + ws-subs (->> (shortcuts->subsections workspace-sc) + (add-translation :sub-sec) + (into (sorted-map-fn))) + + pt-subs (->> (shortcuts->subsections path-sc) + (add-translation :sub-sec) + (into (sorted-map-fn))) + db-subs (->> (shortcuts->subsections dashboard-sc) + (add-translation :sub-sec) + (into (sorted-map-fn))) + vw-subs (->> (shortcuts->subsections viewer-sc) + (add-translation :sub-sec) + (into (sorted-map-fn))) + ws-subs (dissoc ws-subs :basics) + pt-subs (dissoc pt-subs :basics) + db-subs (dissoc db-subs :basics) + vw-subs (dissoc vw-subs :basics) + ws-subs (merge ws-subs pt-subs) + all {:workspace {:children ws-subs + :translation (tr "shortcut-section.workspace")} + :dashboard {:children db-subs + :translation (tr "shortcut-section.dashboard")} + :viewer {:children vw-subs + :translation (tr "shortcut-section.viewer")}} + all (walk all nil) + all-sc-names (map #(translation-keyname :sc %) + (concat (keys workspace-sc) + (keys path-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 class]}] + (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 [class (stl/css :key)] + :key unique-key} char])) + +(mf/defc shortcuts-keys* + [{:keys [content command is-customized has-conflict?]}] + (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 + :class (cond + has-conflict? (stl/css :conflict-key) + is-customized (stl/css :customized-key))}]) + + (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 + :class (cond + has-conflict? (stl/css :conflict-key) + is-customized (stl/css :customized-key))}])])])) + +(mf/defc shortcut-row-editable* + [{:keys [elements custom-shortcuts command-translate conflicts section-key]}] + (let [{:keys [workspace-sc-trans dashboard-sc-trans viewer-sc-trans]} (mf/use-ctx ctx/shortcuts-ctx) + section-shortcuts (case section-key + (:workspace :path) workspace-sc-trans + :dashboard dashboard-sc-trans + :viewer viewer-sc-trans + workspace-sc-trans) + sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements)) + [command command-info] sc-by-translate + content (or (:show-command command-info) (:command command-info)) + group-map (if (= section-key :basics) + (merge (get custom-shortcuts :workspace) + (get custom-shortcuts :dashboard) + (get custom-shortcuts :viewer)) + (get custom-shortcuts section-key)) + group-map (if (map? group-map) group-map {}) + customized? (contains? group-map command) + has-conflict? (contains? conflicts command) + is-editing* (mf/use-state false) + is-editing (deref is-editing*) + + recording-ref (mf/use-ref nil) + + display-parts* (mf/use-state nil) + display-parts (deref display-parts*) + + recorded-command* (mf/use-state nil) + recorded-command (deref recorded-command*) + + conflict* (mf/use-state nil) + conflict (deref conflict*) + + clean-editing-state + (fn [] + (reset! is-editing* false) + (reset! display-parts* nil) + (reset! recorded-command* nil) + (reset! conflict* nil)) + + effective-section-key (if (= section-key :basics) :workspace section-key) + + on-reset-shortcut + (mf/use-fn + (mf/deps effective-section-key command-info) + (fn [shortcut-key] + (st/emit! (customize/reset-custom-shortcut shortcut-key (:original-command command-info) effective-section-key)) + (clean-editing-state))) + + start-editing + (mf/use-fn + (fn [_] + (reset! is-editing* true) + (reset! display-parts* nil) + (reset! recorded-command* nil) + (reset! conflict* nil))) + + stop-editing + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (clean-editing-state))) + + disabled-shortcut + (mf/use-fn + (mf/deps command effective-section-key) + (fn [event] + (dom/prevent-default event) + (st/emit! (customize/set-custom-shortcut + command + "" + nil + effective-section-key)) + (clean-editing-state))) + + save-fn + (mf/use-fn + (mf/deps recorded-command conflict command effective-section-key) + (fn [event] + (dom/prevent-default event) + (when recorded-command + (st/emit! (customize/set-custom-shortcut + command + recorded-command + (:key conflict) + effective-section-key))) + (clean-editing-state))) + + on-editable-container-blur + (mf/use-fn + (fn [e] + (when-not (.contains (.-currentTarget e) + (.-relatedTarget e)) + (clean-editing-state))))] + + (mf/with-effect [is-editing] + (when is-editing + (some-> (mf/ref-val recording-ref) (dom/focus!)))) + + (mf/with-effect [is-editing] + (when is-editing + (letfn [(on-keydown [^js event] + (.preventDefault event) + (.stopPropagation event) + (let [recorded-command (keyboard-event->mousetrap event) + parts (keyboard-event->display-parts event)] + (reset! display-parts* parts) + (when recorded-command + (reset! recorded-command* recorded-command) + (reset! conflict* (find-conflict recorded-command section-shortcuts command)))))] + (->> (events/listen (mf/ref-val recording-ref) "keydown" on-keydown) + (partial events/unlistenByKey))))) + + + [:li {:class (stl/css-case :shortcuts-name-editable true + :shortcuts-editing is-editing + :customized customized?) + :data-customized (str customized?) + :data-conflict (str has-conflict?) + :aria-label command-translate + :key command-translate} + [:button {:on-click start-editing + :disabled is-editing + :aria-label (dm/str "Edit " command-translate) + :class (stl/css-case :shortcut-button-editable true + :shortcut-button-editing is-editing)} + [:span {:class (stl/css :command-name) + :id (dm/str command-translate "-label")} + command-translate] + [:div {:class (stl/css :shortcut-actions) + :aria-labelledby (dm/str command-translate "-label")} + (if (and customized? (str/blank? content)) + [:> icon* {:icon-id i/broken-link + :size "s" + :class (stl/css :shortcut-detach-icon)}] + + (if has-conflict? + (let [conflicting-key (get conflicts command) + conflict-name (translation-keyname :sc conflicting-key)] + [:> tooltip* {:content (tr "shortcuts.overwritten-by" conflict-name) + :id (dm/str (name command) "-conflict-tooltip") + :class (stl/css :option-text)} + [:> shortcuts-keys* {:content content + :command command + :is-customized customized? + :has-conflict? has-conflict?}]]) + [:> shortcuts-keys* {:content content + :command command + :is-customized customized? + :has-conflict? has-conflict?}]))]] + (when is-editing + [:div {:class (stl/css :shortcut-editing) + :ref recording-ref + :tab-index 0 + :on-blur on-editable-container-blur} + [:div {:class (stl/css :shortcut-recording-area)} + (if (nil? display-parts) + [:span {:class (stl/css :shortcut-placeholder)} + (tr "shortcuts.key-combo")] + [:span {:class (stl/css :shortcut-recorded-keys)} + (for [mod (:modifiers display-parts)] + [:span {:class (stl/css-case :key true + :customized-key customized?) :key mod} mod]) + (when (:final-key display-parts) + [:span {:class (stl/css-case :key true + :customized-key customized?)} (:final-key display-parts)]) + (when-not (:finalized? display-parts) + [:span {:class (stl/css :recording-ellipsis)} "..."])])] + (when recorded-command + (if conflict + [:> context-notification* {:level :warning :class (stl/css :modal-error-msg)} + (tr "shortcuts.edit-modal.conflict" (:name conflict))] + [:> context-notification* {:level :success :class (stl/css :modal-success-msg)} + (tr "shortcuts.edit-modal.success")])) + [:div {:class (stl/css :edit-buttons)} + [:div {:class (stl/css :confirmation-buttons)} + [:> icon-button* {:variant "secondary" + :aria-label (tr "labels.cancel") + :on-click stop-editing + :icon i/close + :icon-size "m"}] + [:> icon-button* {:variant "primary" + :aria-label (tr "labels.save") + :on-click save-fn + :icon i/tick + :icon-size "m"}]] + [:div {:class (stl/css :confirmation-buttons)} + [:> icon-button* {:variant "secondary" + :aria-label (tr "shortcuts.disable") + :on-click (fn [e] + (dom/stop-propagation e) + (disabled-shortcut command)) + :icon i/broken-link + :icon-size "m"}] + (when customized? + [:> icon-button* {:variant "secondary" + :aria-label (tr "shortcuts.reset") + :on-click (fn [e] + (dom/stop-propagation e) + (on-reset-shortcut command)) + :icon i/reload + :icon-size "m"}])]]])])) + +(mf/defc shortcut-row* + [{:keys [elements filter-term is-match-section is-match-subsection + editable? custom-shortcuts section-key conflicts hidden subsection-name]}] + (let [shortcut-translations (->> elements vals (map :translation) sort) + 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) + trigger-ref (mf/use-ref nil)] + + [:ul {:class (stl/css :sub-menu) + :id (dm/str (d/name subsection-name) "-subsection") + :hidden hidden} + (for [command-translate sorted-filtered] + (let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements)) + [command command-info] sc-by-translate + content (or (:show-command command-info) (:command command-info)) + group-map (if (= section-key :basics) + (merge (get custom-shortcuts :workspace) + (get custom-shortcuts :dashboard) + (get custom-shortcuts :viewer)) + (get custom-shortcuts section-key)) + group-map (if (map? group-map) group-map {}) + customized? (contains? group-map command) + has-conflict? (contains? conflicts command)] + (if editable? + [:> shortcut-row-editable* {:elements elements + :custom-shortcuts custom-shortcuts + :section-key section-key + :key command-translate + :command-translate command-translate + :conflicts conflicts}] + [:li {:class (stl/css-case :shortcuts-name true + :customized customized?) + :data-customized (str customized?) + :data-conflict (str has-conflict?) + :aria-label command-translate + :key command-translate} + [:span {:class (stl/css :command-name) + :id (dm/str command-translate "-label")} + command-translate] + [:div {:class (stl/css :shortcut-actions) + :aria-labelledby (dm/str command-translate "-label")} + (if (and customized? (str/blank? content)) + [:> tooltip* {:content (tr "shortcuts.disabled-shortcut") + :trigger-ref trigger-ref + :id (dm/str command-translate "-tooltip") + :class (stl/css :option-text)} + [:> icon* {:icon-id i/broken-link + :aria-labelledby (dm/str command-translate "-tooltip") + :size "s" + :ref trigger-ref + :class (stl/css :shortcut-detach-icon)}]] + (if has-conflict? + (let [conflicting-key (get conflicts command) + conflict-name (translation-keyname :sc conflicting-key)] + [:> tooltip* {:content (tr "shortcuts.overwritten-by" conflict-name) + :id (dm/str (name command) "-conflict-tooltip") + :class (stl/css :option-text)} + [:> shortcuts-keys* {:content content + :command command + :is-customized customized? + :has-conflict? has-conflict?}]]) + [:> shortcuts-keys* {:content content + :command command + :is-customized customized? + :has-conflict? has-conflict?}]))]])))])) + + +(mf/defc section-title* + [{:keys [name is-visible is-sub on-click]}] + (let [handle-key-down + (fn [event] + (when (or (= "Enter" (.-key event)) + (= " " (.-key event))) + (.preventDefault event) + (on-click event)))] + [:button {:class (if is-sub + (stl/css :subsection-title) + (stl/css :section-title)) + :on-click on-click + :on-key-down handle-key-down + :aria-expanded is-visible + :aria-controls (dm/str (str/lower name) (if is-sub "-subsection" "-section"))} + [:> icon* {:icon-id (if is-visible i/arrow-down i/arrow-right) + :size "s" + :aria-hidden true}] + [: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 section-key conflicts hidden]}] + (if (= :none (first (keys subsections))) + [:> shortcut-row* {:elements (:children (:none subsections)) + :filter-term filter-term + :is-match-section is-match-section + :is-match-subsection true + :editable? editable? + :hidden hidden + :custom-shortcuts custom-shortcuts + :section-key section-key + :conflicts conflicts}] + + [:ul {:class (stl/css :subsection-menu) + :hidden hidden + :id (dm/str (name section-key) "-section")} + (let [sorted-subs (sort-by (fn [[_ v]] (:translation v)) subsections)] + (for [[sub-name sub-info] sorted-subs] + (let [visible? (some #(= % (:id sub-info)) open-sections)] + [:li {:key (name sub-name)} + [:h4 + [:> section-title* {:name (:translation sub-info) + :is-visible visible? + :is-sub true + :on-click (manage-sections (:id sub-info))}]] + + [:> shortcut-row* {:elements (:children sub-info) + :subsection-name sub-name + :filter-term filter-term + :is-match-section is-match-section + :is-match-subsection true + :editable? editable? + :hidden (not visible?) + :custom-shortcuts custom-shortcuts + :section-key section-key + :conflicts conflicts}]])))])) + + + +(mf/defc shortcut-section* + [{:keys [section manage-sections open-sections filter-term + editable? custom-shortcuts conflicts]}] + (let [[section-key section-info] section + section-id (:id section-info) + section-translation (:translation section-info) + subsections (:children section-info) + visible? (some #(= % section-id) open-sections)] + [:div {:class (stl/css :section)} + [:h3 {:class (stl/css :section-header)} + [:> section-title* {:name section-translation + :is-visible visible? + :is-sub false + :on-click (manage-sections section-id)}]] + + [:> shortcut-subsection* {:subsections subsections + :open-sections open-sections + :manage-sections manage-sections + :is-match-section false + :filter-term filter-term + :editable? editable? + :hidden (not visible?) + :custom-shortcuts custom-shortcuts + :section-key section-key + :conflicts conflicts}]])) diff --git a/frontend/src/app/main/ui/shortcuts.scss b/frontend/src/app/main/ui/shortcuts.scss new file mode 100644 index 0000000000..cc78a03214 --- /dev/null +++ b/frontend/src/app/main/ui/shortcuts.scss @@ -0,0 +1,214 @@ +// 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; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/mixins.scss" as *; +@use "ds/typography.scss" as t; + +.section { + margin: 0; +} + +.section-header { + padding: 0; +} + +.section-title, +.subsection-title { + @include t.use-typography("title-small"); + + display: flex; + align-items: center; + margin: 0; + padding: var(--sp-s) 0; + color: var(--color-foreground-secondary); + cursor: pointer; + border: none; + background: none; + + .subsection-name, + .section-name { + padding-inline-start: var(--sp-xs); + } + + &:hover { + color: var(--color-foreground-primary); + } +} + +.subsection-title { + text-transform: none; + padding-inline-start: var(--sp-m); +} + +.subsection-menu { + margin-block-end: var(--sp-s); +} + +.sub-menu { + margin-block-end: var(--sp-s); +} + +.shortcuts-name { + display: flex; + align-items: center; + justify-content: space-between; + inline-size: 100%; + min-block-size: $sz-32; + padding: px2rem(6); + margin-block-end: var(--sp-s); + border-radius: $br-8; + background-color: var(--color-background-tertiary); +} + +.command-name { + @include t.use-typography("body-small"); + + margin-inline-start: var(--sp-xxs); + color: var(--color-foreground-primary); + flex: 1; + min-inline-size: 0; + text-align: start; +} + +// Editable rows + +.shortcuts-name-editable { + inline-size: 100%; + min-block-size: $sz-32; + padding: px2rem(6); + margin-block-end: var(--sp-s); + border-radius: $br-8; + background-color: var(--color-background-tertiary); + + &:hover { + background-color: var(--color-background-quaternary); + } +} + +.shortcuts-editing { + background-color: var(--color-background-quaternary); + display: flex; + flex-direction: column; + gap: var(--sp-s); + + &:focus-within { + border: $b-1 solid var(--color-accent-primary); + } +} + +.shortcut-button-editable { + border: none; + background: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + inline-size: 100%; +} + +.shortcut-button-editing { + gap: var(--sp-s); +} + +.shortcut-editing { + display: flex; + flex-direction: column; + gap: var(--sp-s); + outline: none; +} + +.shortcut-recording-area { + inline-size: 100%; + padding: px2rem(24); + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-background-tertiary); + border-radius: $br-8; + border: $b-1 dashed var(--color-foreground-secondary); +} + +.shortcut-actions { + display: flex; + align-items: center; + gap: var(--sp-xxs); +} + +.edit-buttons { + display: flex; + align-items: center; + transition: opacity 0.15s ease; + justify-content: space-between; + flex-direction: row-reverse; +} + +.confirmation-buttons { + display: flex; + align-items: center; + gap: var(--sp-s); + justify-content: flex-end; +} + +.keys { + display: flex; + justify-content: center; + align-items: center; + gap: var(--sp-xxs); +} + +.key { + @include t.use-typography("body-small"); + + display: flex; + justify-content: center; + align-items: center; + text-transform: capitalize; + block-size: px2rem(20); + padding: px2rem(2) px2rem(6); + border-radius: $br-6; + color: var(--color-foreground-primary); + background-color: var(--color-background-primary); +} + +.customized-key { + background-color: var(--color-background-info); + color: var(--color-foreground-primary); +} + +.conflict-key { + background-color: var(--color-background-error); + color: var(--color-foreground-error); +} + +.space { + margin: 0 var(--sp-xxs); + color: var(--color-foreground-secondary); +} + +.shortcut-placeholder { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); +} + +.shortcut-recorded-keys { + display: flex; + justify-content: center; + align-items: center; + gap: var(--sp-xxs); +} + +.shortcut-detach-icon { + color: var(--color-foreground-secondary); +} + +.recording-ellipsis { + color: var(--color-foreground-secondary); +} diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 2f997ac7ee..b1ae49b3b6 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -406,7 +406,7 @@ (when (not (dom/fullscreen?)) (st/emit! (dv/exit-fullscreen)))))] - (hooks/use-shortcuts ::viewer sc/shortcuts) + (hooks/use-shortcuts ::viewer sc/shortcuts :viewer) (when (nil? page) (ex/raise :type :not-found)) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index de586da6d8..0498e2d656 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -788,7 +788,7 @@ (combine-groups-with-resolved color-tokens)))] (mf/with-effect [] - (st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts))) + (st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts :workspace))) (fn [] (st/emit! (dsc/pop-shortcuts ::colorpicker)) (when (and @dirty? @last-change on-close) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index c73ac2624a..de3cb44049 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -56,9 +56,14 @@ (mf/defc menu-entry* {::mf/private true} - [{:keys [title shortcut on-click on-pointer-enter on-pointer-leave + [{:keys [title shortcut shortcut-key on-click on-pointer-enter on-pointer-leave on-unmount children is-selected icon disabled value]}] - (let [submenu-ref (mf/use-ref nil) + (let [cs (mf/deref refs/custom-shortcuts) + customized? (and shortcut-key + (let [c (get-in cs [:workspace shortcut-key])] + (and c (not= c "")))) + + submenu-ref (mf/use-ref nil) hovering? (mf/use-ref false) on-click' @@ -128,7 +133,8 @@ [:span {:class (stl/css :shortcut)} (for [[idx sc] (d/enumerate (scd/split-sc shortcut))] [:span {:key (dm/str shortcut "-" idx) - :class (stl/css :shortcut-key)} sc])]) + :class (stl/css-case :shortcut-key true + :customized-key customized?)} sc])]) (when (> (count children) 1) [:span {:class (stl/css :submenu-icon)} @@ -149,7 +155,10 @@ (mf/defc context-menu-edit* {::mf/private true} [{:keys [shapes]}] - (let [multiple? (> (count shapes) 1) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + multiple? (> (count shapes) 1) do-copy #(st/emit! (dw/copy-selected)) do-copy-link #(st/emit! (dw/copy-link-to-clipboard)) @@ -207,19 +216,24 @@ [:> menu-entry* {:title (tr "workspace.shape.menu.copy-id") :on-click do-copy-id}]) [:> menu-entry* {:title (tr "workspace.shape.menu.copy") - :shortcut (sc/get-tooltip :copy) + :shortcut (get-tt :copy) + :shortcut-key :copy :on-click do-copy}] [:> menu-entry* {:title (tr "workspace.shape.menu.copy-link") - :shortcut (sc/get-tooltip :copy-link) + :shortcut (get-tt :copy-link) + :shortcut-key :copy-link :on-click do-copy-link}] [:> menu-entry* {:title (tr "workspace.shape.menu.cut") - :shortcut (sc/get-tooltip :cut) + :shortcut (get-tt :cut) + :shortcut-key :cut :on-click do-cut}] [:> menu-entry* {:title (tr "workspace.shape.menu.paste") - :shortcut (sc/get-tooltip :paste) + :shortcut (get-tt :paste) + :shortcut-key :paste :on-click do-paste}] [:> menu-entry* {:title (tr "workspace.shape.menu.duplicate") - :shortcut (sc/get-tooltip :duplicate) + :shortcut (get-tt :duplicate) + :shortcut-key :duplicate :on-click do-duplicate}] [:> menu-entry* {:title (tr "workspace.shape.menu.copy-paste-as") @@ -242,11 +256,13 @@ :on-click handle-copy-text}] [:> menu-entry* {:title (tr "workspace.shape.menu.copy-props") - :shortcut (sc/get-tooltip :copy-props) + :shortcut (get-tt :copy-props) + :shortcut-key :copy-props :disabled multiple? :on-click handle-copy-props}] [:> menu-entry* {:title (tr "workspace.shape.menu.paste-props") - :shortcut (sc/get-tooltip :paste-props) + :shortcut (get-tt :paste-props) + :shortcut-key :paste-props :disabled (and (cf/check-browser? :chrome) (not @enabled-paste-props*)) :on-click handle-paste-props}]] @@ -255,7 +271,10 @@ (mf/defc context-menu-layer-position* {::mf/private true} [{:keys [shapes]}] - (let [do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up))) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + do-bring-forward (mf/use-fn #(st/emit! (dw/vertical-order-selected :up))) do-bring-to-front (mf/use-fn #(st/emit! (dw/vertical-order-selected :top))) do-send-backward (mf/use-fn #(st/emit! (dw/vertical-order-selected :down))) do-send-to-back (mf/use-fn #(st/emit! (dw/vertical-order-selected :bottom))) @@ -284,16 +303,20 @@ :on-unmount (on-unmount (:id object)) :icon (usi/get-shape-icon object)}])]) [:> menu-entry* {:title (tr "workspace.shape.menu.forward") - :shortcut (sc/get-tooltip :bring-forward) + :shortcut (get-tt :bring-forward) + :shortcut-key :bring-forward :on-click do-bring-forward}] [:> menu-entry* {:title (tr "workspace.shape.menu.front") - :shortcut (sc/get-tooltip :bring-front) + :shortcut (get-tt :bring-front) + :shortcut-key :bring-front :on-click do-bring-to-front}] [:> menu-entry* {:title (tr "workspace.shape.menu.backward") - :shortcut (sc/get-tooltip :bring-backward) + :shortcut (get-tt :bring-backward) + :shortcut-key :bring-backward :on-click do-send-backward}] [:> menu-entry* {:title (tr "workspace.shape.menu.back") - :shortcut (sc/get-tooltip :bring-back) + :shortcut (get-tt :bring-back) + :shortcut-key :bring-back :on-click do-send-to-back}] [:> menu-separator* {}]])) @@ -301,22 +324,30 @@ (mf/defc context-menu-flip* {::mf/private true} [] - (let [do-flip-vertical #(st/emit! (dw/flip-vertical-selected)) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + do-flip-vertical #(st/emit! (dw/flip-vertical-selected)) do-flip-horizontal #(st/emit! (dw/flip-horizontal-selected))] [:* [:> menu-entry* {:title (tr "workspace.shape.menu.flip-vertical") - :shortcut (sc/get-tooltip :flip-vertical) + :shortcut (get-tt :flip-vertical) + :shortcut-key :flip-vertical :on-click do-flip-vertical}] [:> menu-entry* {:title (tr "workspace.shape.menu.flip-horizontal") - :shortcut (sc/get-tooltip :flip-horizontal) + :shortcut (get-tt :flip-horizontal) + :shortcut-key :flip-horizontal :on-click do-flip-horizontal}] [:> menu-separator* {}]])) (mf/defc context-menu-thumbnail* {::mf/private true} [{:keys [shapes]}] - (let [single? (= (count shapes) 1) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + single? (= (count shapes) 1) has-frame? (some cfh/frame-shape? shapes) do-toggle-thumbnail #(st/emit! (dw/toggle-file-thumbnail-selected))] (when (and single? has-frame?) @@ -325,25 +356,33 @@ [:> menu-entry* {:title (tr "workspace.shape.menu.thumbnail-remove") :on-click do-toggle-thumbnail}] [:> menu-entry* {:title (tr "workspace.shape.menu.thumbnail-set") - :shortcut (sc/get-tooltip :thumbnail-set) + :shortcut (get-tt :thumbnail-set) + :shortcut-key :thumbnail-set :on-click do-toggle-thumbnail}]) [:> menu-separator* {}]]))) (mf/defc context-menu-rename* {::mf/private true} [{:keys [shapes]}] - (let [do-rename #(st/emit! (dw/start-rename-selected))] + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + do-rename #(st/emit! (dw/start-rename-selected))] (when (= (count shapes) 1) [:* [:> menu-separator* {}] [:> menu-entry* {:title (tr "workspace.shape.menu.rename") - :shortcut (sc/get-tooltip :rename) + :shortcut (get-tt :rename) + :shortcut-key :rename :on-click do-rename}]]))) (mf/defc context-menu-group* {::mf/private true} [{:keys [shapes]}] - (let [multiple? (> (count shapes) 1) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + multiple? (> (count shapes) 1) single? (= (count shapes) 1) objects (deref refs/workspace-page-objects) @@ -371,44 +410,56 @@ [:* (when (or has-bool? has-group? has-mask? has-frame?) [:> menu-entry* {:title (tr "workspace.shape.menu.ungroup") - :shortcut (sc/get-tooltip :ungroup) + :shortcut (get-tt :ungroup) + :shortcut-key :ungroup :on-click do-remove-group}]) [:> menu-entry* {:title (tr "workspace.shape.menu.group") - :shortcut (sc/get-tooltip :group) + :shortcut (get-tt :group) + :shortcut-key :group :on-click do-create-group}] (when (or multiple? (and is-group? (not has-mask?)) is-bool?) [:> menu-entry* {:title (tr "workspace.shape.menu.mask") - :shortcut (sc/get-tooltip :mask) + :shortcut (get-tt :mask) + :shortcut-key :mask :on-click do-mask-group}]) (when has-mask? [:> menu-entry* {:title (tr "workspace.shape.menu.unmask") - :shortcut (sc/get-tooltip :unmask) + :shortcut (get-tt :unmask) + :shortcut-key :unmask :on-click do-unmask-group}]) [:> menu-entry* {:title (tr "workspace.shape.menu.create-artboard-from-selection") - :shortcut (sc/get-tooltip :artboard-selection) + :shortcut (get-tt :artboard-selection) + :shortcut-key :artboard-selection :on-click do-create-artboard-from-selection}] [:> menu-separator* {}]])])) (mf/defc context-focus-mode-menu* {::mf/private true} [] - (let [focus (mf/deref refs/workspace-focus-selected) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + focus (mf/deref refs/workspace-focus-selected) do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))] [:> menu-entry* {:title (if (empty? focus) (tr "workspace.focus.focus-on") (tr "workspace.focus.focus-off")) - :shortcut (sc/get-tooltip :toggle-focus-mode) + :shortcut (get-tt :toggle-focus-mode) + :shortcut-key :toggle-focus-mode :on-click do-toggle-focus-mode}])) (mf/defc context-menu-path* {::mf/private true} [{:keys [shapes objects disable-flatten disable-booleans]}] - (let [multiple? (> (count shapes) 1) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + multiple? (> (count shapes) 1) single? (= (count shapes) 1) has-group? (->> shapes (d/seek cfh/group-shape?)) @@ -446,7 +497,8 @@ [:* (when (and single? (not is-frame?)) [:> menu-entry* {:title (tr "workspace.shape.menu.edit") - :shortcut (sc/get-tooltip :start-editing) + :shortcut (get-tt :start-editing) + :shortcut-key :start-editing :on-click do-start-editing}]) (when-not (or disable-flatten has-frame? has-path?) @@ -463,16 +515,20 @@ (or multiple? (and single? (or is-group? is-bool?)))) [:> menu-entry* {:title (tr "workspace.shape.menu.path")} [:> menu-entry* {:title (tr "workspace.shape.menu.union") - :shortcut (sc/get-tooltip :bool-union) + :shortcut (get-tt :bool-union) + :shortcut-key :bool-union :on-click (make-do-bool :union)}] [:> menu-entry* {:title (tr "workspace.shape.menu.difference") - :shortcut (sc/get-tooltip :bool-difference) + :shortcut (get-tt :bool-difference) + :shortcut-key :bool-difference :on-click (make-do-bool :difference)}] [:> menu-entry* {:title (tr "workspace.shape.menu.intersection") - :shortcut (sc/get-tooltip :bool-intersection) + :shortcut (get-tt :bool-intersection) + :shortcut-key :bool-intersection :on-click (make-do-bool :intersection)}] [:> menu-entry* {:title (tr "workspace.shape.menu.exclude") - :shortcut (sc/get-tooltip :bool-exclude) + :shortcut (get-tt :bool-exclude) + :shortcut-key :bool-exclude :on-click (make-do-bool :exclude)}] (when (and single? is-bool? (not disable-flatten)) @@ -484,7 +540,10 @@ (mf/defc context-menu-layer-options* {::mf/private true} [{:keys [shapes]}] - (let [ids (mapv :id shapes) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + ids (mapv :id shapes) do-show-shape #(st/emit! (dw/update-shape-flags ids {:hidden false})) do-hide-shape #(st/emit! (dw/update-shape-flags ids {:hidden true})) do-lock-shape #(st/emit! (dw/update-shape-flags ids {:blocked true})) @@ -492,18 +551,22 @@ [:* (if (every? :hidden shapes) [:> menu-entry* {:title (tr "workspace.shape.menu.show") - :shortcut (sc/get-tooltip :toggle-visibility) + :shortcut (get-tt :toggle-visibility) + :shortcut-key :toggle-visibility :on-click do-show-shape}] [:> menu-entry* {:title (tr "workspace.shape.menu.hide") - :shortcut (sc/get-tooltip :toggle-visibility) + :shortcut (get-tt :toggle-visibility) + :shortcut-key :toggle-visibility :on-click do-hide-shape}]) (if (every? :blocked shapes) [:> menu-entry* {:title (tr "workspace.shape.menu.unlock") - :shortcut (sc/get-tooltip :toggle-lock) + :shortcut (get-tt :toggle-lock) + :shortcut-key :toggle-lock :on-click do-unlock-shape}] [:> menu-entry* {:title (tr "workspace.shape.menu.lock") - :shortcut (sc/get-tooltip :toggle-lock) + :shortcut (get-tt :toggle-lock) + :shortcut-key :toggle-lock :on-click do-lock-shape}])])) (mf/defc context-menu-prototype* @@ -530,7 +593,10 @@ (mf/defc context-menu-layout* {::mf/private true} [{:keys [shapes]}] - (let [single? (= (count shapes) 1) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + single? (= (count shapes) 1) objects (deref refs/workspace-page-objects) any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes)) @@ -565,28 +631,35 @@ [:> menu-separator* {}] (if has-flex? [:> menu-entry* {:title (tr "workspace.shape.menu.remove-flex") - :shortcut (sc/get-tooltip :toggle-layout-flex) + :shortcut (get-tt :toggle-layout-flex) + :shortcut-key :toggle-layout-flex :on-click on-remove-layout}] [:> menu-entry* {:title (tr "workspace.shape.menu.remove-grid") - :shortcut (sc/get-tooltip :toggle-layout-grid) + :shortcut (get-tt :toggle-layout-grid) + :shortcut-key :toggle-layout-grid :on-click on-remove-layout}])] (when (or single? (not any-is-variant?)) [:div [:> menu-separator* {}] [:> menu-entry* {:title (tr "workspace.shape.menu.add-flex") - :shortcut (sc/get-tooltip :toggle-layout-flex) + :shortcut (get-tt :toggle-layout-flex) + :shortcut-key :toggle-layout-flex :value "flex" :on-click on-add-layout}] [:> menu-entry* {:title (tr "workspace.shape.menu.add-grid") - :shortcut (sc/get-tooltip :toggle-layout-grid) + :shortcut (get-tt :toggle-layout-grid) + :shortcut-key :toggle-layout-grid :value "grid" :on-click on-add-layout}]])))])) (mf/defc context-menu-component* {:mf/private true} [{:keys [shapes]}] - (let [single? (= (count shapes) 1) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + single? (= (count shapes) 1) objects (deref refs/workspace-page-objects) can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) shapes)) components-menu-entries (cmm/generate-components-menu-entries shapes) @@ -608,7 +681,8 @@ [:> menu-separator* {}] [:> menu-entry* {:title (tr "workspace.shape.menu.create-component") - :shortcut (sc/get-tooltip :create-component-variant) + :shortcut (get-tt :create-component-variant) + :shortcut-key :create-component-variant :on-click do-add-component}] (when (not single?) [:> menu-entry* {:title (tr "workspace.shape.menu.create-multiple-components") @@ -618,17 +692,19 @@ [:* [:> menu-separator*] (for [entry (filter some? components-menu-entries)] - [:> menu-entry* {:key (:title entry) - :title (:title entry) - :shortcut (when (contains? entry :shortcut) - (sc/get-tooltip (:shortcut entry))) - :on-click (:action entry)}])]) + (let [sc-key (:shortcut entry)] + [:> menu-entry* {:key (:title entry) + :title (:title entry) + :shortcut (when sc-key (get-tt sc-key)) + :shortcut-key sc-key + :on-click (:action entry)}]))]) (when variant-container? [:* [:> menu-separator*] [:> menu-entry* {:title (tr "workspace.shape.menu.add-variant") - :shortcut (sc/get-tooltip :create-component-variant) + :shortcut (get-tt :create-component-variant) + :shortcut-key :create-component-variant :on-click do-add-variant}]]) (when (and (not single?) all-main? (not any-variant?)) @@ -659,11 +735,15 @@ (mf/defc context-menu-delete* {::mf/private true} [] - (let [do-delete #(st/emit! (dw/delete-selected))] + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + do-delete #(st/emit! (dw/delete-selected))] [:* [:> menu-separator* {}] [:> menu-entry* {:title (tr "workspace.shape.menu.delete") - :shortcut (sc/get-tooltip :delete) + :shortcut (get-tt :delete) + :shortcut-key :delete :on-click do-delete}]])) (mf/defc shape-context-menu* @@ -741,7 +821,10 @@ (mf/defc viewport-context-menu* [] - (let [focus (mf/deref refs/workspace-focus-selected) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + + focus (mf/deref refs/workspace-focus-selected) read-only? (mf/use-ctx ctx/workspace-read-only?) do-paste #(st/emit! (dw/paste-from-clipboard)) do-hide-ui #(st/emit! (-> (dw/toggle-layout-flag :hide-ui) @@ -750,15 +833,18 @@ [:* (when-not ^boolean read-only? [:> menu-entry* {:title (tr "workspace.shape.menu.paste") - :shortcut (sc/get-tooltip :paste) + :shortcut (get-tt :paste) + :shortcut-key :paste :on-click do-paste}]) [:> menu-entry* {:title (tr "workspace.shape.menu.hide-ui") - :shortcut (sc/get-tooltip :hide-ui) + :shortcut (get-tt :hide-ui) + :shortcut-key :hide-ui :on-click do-hide-ui}] (when (d/not-empty? focus) [:> menu-entry* {:title (tr "workspace.focus.focus-off") - :shortcut (sc/get-tooltip :toggle-focus-mode) + :shortcut (get-tt :toggle-focus-mode) + :shortcut-key :toggle-focus-mode :on-click do-toggle-focus-mode}])])) (mf/defc grid-track-context-menu* diff --git a/frontend/src/app/main/ui/workspace/context_menu.scss b/frontend/src/app/main/ui/workspace/context_menu.scss index 212a7ae562..330d36fc65 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.scss +++ b/frontend/src/app/main/ui/workspace/context_menu.scss @@ -21,8 +21,8 @@ width: deprecated.$s-240; padding: deprecated.$s-4; border-radius: deprecated.$br-8; - border: deprecated.$s-2 solid var(--panel-border-color); - background-color: var(--menu-background-color); + border: deprecated.$s-2 solid var(--color-background-quaternary); + background-color: var(--color-background-tertiary); max-height: 100vh; overflow-y: auto; } @@ -36,6 +36,12 @@ } .context-menu-item { + --context-menu-item-bg-color: var(--color-background-tertiary); + --context-menu-item-title-color: var(--color-foreground-primary); + --context-menu-item-shortcut-color: var(--color-foreground-secondary); + --context-menu-item-shortcut-bg-color: var(--color-background-primary); + --context-menu-item-shortcut-border-color: transparent; + display: flex; align-items: center; justify-content: space-between; @@ -44,54 +50,50 @@ padding: deprecated.$s-6; border-radius: deprecated.$br-8; cursor: pointer; - - .title { - @include deprecated.body-small-typography; - - color: var(--menu-foreground-color); - } - - .shortcut { - @include deprecated.flex-center; - - gap: deprecated.$s-2; - color: var(--menu-shortcut-foreground-color); - - .shortcut-key { - @include deprecated.body-small-typography; - @include deprecated.flex-center; - - height: deprecated.$s-20; - padding: deprecated.$s-2 deprecated.$s-6; - border-radius: deprecated.$br-6; - background-color: var(--menu-shortcut-background-color); - } - } - - .submenu-icon svg { - @extend %button-icon-small; - - stroke: var(--menu-foreground-color); - } + background-color: var(--context-menu-item-bg-color); + border: 1px solid var(--context-menu-item-shortcut-border-color); &:hover { - background-color: var(--menu-background-color-hover); - - .title { - color: var(--menu-foreground-color-hover); - } - - .shortcut { - color: var(--menu-shortcut-foreground-color-hover); - } + --context-menu-item-title-color: var(--menu-foreground-color-hover); + --context-menu-item-bg-color: var(--menu-background-color-hover); + --context-menu-item-shortcut-color: var(--menu-shortcut-foreground-color-hover); } &:focus { - border: 1px solid var(--menu-border-color-focus); - background-color: var(--menu-background-color-focus); + --context-menu-item-bg-color: var(--menu-background-color-focus); + --context-menu-item-shortcut-border-color: var(--menu-border-color-focus); } } +.title { + @include deprecated.body-small-typography; + + color: var(--context-menu-item-title-color); +} + +.shortcut { + @include deprecated.flex-center; + + gap: deprecated.$s-2; + color: var(--context-menu-item-shortcut-color); +} + +.shortcut-key { + @include deprecated.body-small-typography; + @include deprecated.flex-center; + + height: deprecated.$s-20; + padding: deprecated.$s-2 deprecated.$s-6; + border-radius: deprecated.$br-6; + background-color: var(--context-menu-item-shortcut-bg-color); +} + +.submenu-icon svg { + @extend %button-icon-small; + + stroke: var(--menu-foreground-color); +} + .icon-menu-item { display: flex; justify-content: flex-start; @@ -165,3 +167,8 @@ border: deprecated.$s-2 solid var(--menu-foreground-color); } } + +.customized-key { + background-color: var(--color-background-info); + color: var(--color-foreground-primary); +} diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 7a33b98024..bb2c6a2d88 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -51,11 +51,15 @@ (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])]) + (let [custom-shortcuts (mf/deref refs/custom-shortcuts) + customized? (let [c (get-in custom-shortcuts [:workspace id])] + (and c (not= c "")))] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-effective-tooltip id custom-shortcuts))] + [:span {:class (stl/css-case :shortcut-key true + :customized-key customized?) + :key sc} + sc])])) (mf/defc help-info-menu* {::mf/private true @@ -982,6 +986,19 @@ ::ev/origin "workspace:menu"}) (modal/show :plugin-management {})))) + show-shortcuts + (mf/use-fn + (mf/deps layout) + (fn [event] + (dom/stop-propagation event) + (reset! show-menu* false) + (reset! selected-sub-menu* nil) + (when (contains? layout :collapse-left-sidebar) + (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) + (st/emit! + (-> (dw/toggle-layout-flag :shortcuts) + (vary-meta assoc ::ev/origin "workspace-menu"))))) + subscription (:subscription (:props profile)) subscription-type (get-subscription-type subscription)] @@ -1148,6 +1165,7 @@ :toggle-flag toggle-flag :toggle-theme toggle-theme :toggle-render toggle-render + :show-shortcuts show-shortcuts :on-close close-sub-menu}] :plugins diff --git a/frontend/src/app/main/ui/workspace/main_menu.scss b/frontend/src/app/main/ui/workspace/main_menu.scss index 610e7e45cc..6a6e7611e0 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.scss +++ b/frontend/src/app/main/ui/workspace/main_menu.scss @@ -182,3 +182,8 @@ border-radius: $br-6; background-color: var(--menu-shortcut-background-color); } + +.customized-key { + background-color: var(--color-background-info); + color: var(--color-foreground-primary); +} diff --git a/frontend/src/app/main/ui/workspace/palette.cljs b/frontend/src/app/main/ui/workspace/palette.cljs index 11ba417e9b..7b5d5250cc 100644 --- a/frontend/src/app/main/ui/workspace/palette.cljs +++ b/frontend/src/app/main/ui/workspace/palette.cljs @@ -51,9 +51,12 @@ (mf/defc palette* [{:keys [layout on-change-size]}] - (let [color-palette? (:colorpalette layout) - text-palette? (:textpalette layout) - hide-palettes? (:hide-palettes layout) + (let [color-palette? (:colorpalette layout) + text-palette? (:textpalette layout) + hide-palettes? (:hide-palettes layout) + + custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) read-only? (mf/use-ctx ctx/workspace-read-only?) container (mf/use-ref nil) @@ -179,16 +182,16 @@ [:ul {:class (dm/str size-classname " " (stl/css-case :palette-btn-list true :hidden-bts hide-palettes?))} [:li {:class (stl/css :palette-item)} - [:button {:title (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) - :aria-label (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette)) + [:button {:title (tr "workspace.toolbar.color-palette" (get-tt :toggle-colorpalette)) + :aria-label (tr "workspace.toolbar.color-palette" (get-tt :toggle-colorpalette)) :class (stl/css-case :palette-btn true :selected color-palette?) :on-click on-select-color-palette} deprecated-icon/drop-icon]] [:li {:class (stl/css :palette-item)} - [:button {:title (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) - :aria-label (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette)) + [:button {:title (tr "workspace.toolbar.text-palette" (get-tt :toggle-textpalette)) + :aria-label (tr "workspace.toolbar.text-palette" (get-tt :toggle-textpalette)) :class (stl/css-case :palette-btn true :selected text-palette?) :on-click on-select-text-palette} diff --git a/frontend/src/app/main/ui/workspace/right_header.cljs b/frontend/src/app/main/ui/workspace/right_header.cljs index 947113c5f9..ff5e6a2152 100644 --- a/frontend/src/app/main/ui/workspace/right_header.cljs +++ b/frontend/src/app/main/ui/workspace/right_header.cljs @@ -40,8 +40,10 @@ {::mf/wrap [mf/memo] ::mf/wrap-props false} [{:keys [zoom on-increase on-decrease on-zoom-reset on-zoom-fit on-zoom-selected]}] - (let [open* (mf/use-state false) - open? (deref open*) + (let [open* (mf/use-state false) + open? (deref open*) + custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) open-dropdown (mf/use-fn @@ -97,14 +99,14 @@ :on-click on-zoom-fit} (tr "workspace.header.zoom-fit-all") [:span {:class (stl/css :shortcuts)} - (for [sc (scd/split-sc (sc/get-tooltip :fit-all))] + (for [sc (scd/split-sc (get-tt :fit-all))] [:span {:class (stl/css :shortcut-key) :key (str "zoom-fit-" sc)} sc])]] [:li {:class (stl/css :zoom-option) :on-click on-zoom-selected} (tr "workspace.header.zoom-selected") [:span {:class (stl/css :shortcuts)} - (for [sc (scd/split-sc (sc/get-tooltip :zoom-selected))] + (for [sc (scd/split-sc (get-tt :zoom-selected))] [:span {:class (stl/css :shortcut-key) :key (str "zoom-selected-" sc)} sc])]]]]])) @@ -118,6 +120,9 @@ read-only? (mf/use-ctx ctx/workspace-read-only?) selected-drawtool (mf/deref refs/selected-drawing-tool) + custom-shortcuts (mf/deref refs/custom-shortcuts) + get-tt #(sc/get-effective-tooltip % custom-shortcuts) + on-increase (mf/use-fn #(st/emit! (dw/increase-zoom nil))) on-decrease (mf/use-fn #(st/emit! (dw/decrease-zoom nil))) on-zoom-reset (mf/use-fn #(st/emit! dw/reset-zoom)) @@ -211,8 +216,8 @@ :on-zoom-selected on-zoom-selected}]] [:div {:class (stl/css :comments-section)} - [:button {:title (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment)) - :aria-label (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment)) + [:button {:title (tr "workspace.toolbar.comments" (get-tt :add-comment)) + :aria-label (tr "workspace.toolbar.comments" (get-tt :add-comment)) :class (stl/css-case :comments-btn true :selected (= selected-drawtool :comments)) :on-click toggle-comments @@ -239,7 +244,7 @@ deprecated-icon/share]) [:a {:class (stl/css :viewer-btn) - :title (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer)) + :title (tr "workspace.header.viewer" (get-tt :open-viewer)) :on-click nav-to-viewer} deprecated-icon/play]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index 0f6cc60077..00b8b08223 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -253,7 +253,7 @@ (on-select @selected)) (mf/with-effect [] - (st/emit! (dsc/push-shortcuts :typography {})) + (st/emit! (dsc/push-shortcuts :typography {} :workspace)) (fn [] (st/emit! (dsc/pop-shortcuts :typography)))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs index aecf386f6a..be454c585c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs @@ -10,450 +10,80 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.config :as cf] - [app.main.data.dashboard.shortcuts] + [app.main.data.dashboard.shortcuts :as dsc] [app.main.data.shortcuts :as ds] - [app.main.data.viewer.shortcuts] + [app.main.data.viewer.shortcuts :as vsc] [app.main.data.workspace :as dw] - [app.main.data.workspace.path.shortcuts] - [app.main.data.workspace.shortcuts] + [app.main.data.workspace.path.shortcuts :as psc] + [app.main.data.workspace.shortcuts :as wsc] + [app.main.refs :as refs] + [app.main.router :as rt] [app.main.store :as st] + [app.main.ui.components.button-link :as bl] [app.main.ui.components.search-bar :refer [search-bar*]] - [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.ds.product.panel-title :refer [panel-title*]] + [app.main.ui.shortcuts :as ss] [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]}] - (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))] - [:li {:class (stl/css :shortcuts-name) - :key command-translate} - [:span {:class (stl/css :command-name)} - command-translate] - [:> shortcuts-keys* {:content content - :command command}]]))])) - -(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]}] - (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}]) - - [: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?}]]])))]))) - -(mf/defc shortcut-section* - [{:keys [section manage-sections open-sections filter-term]}] - (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}]]]))) (mf/defc shortcuts-container* [{:keys [class]}] - (let [workspace-shortcuts app.main.data.workspace.shortcuts/shortcuts - path-shortcuts app.main.data.workspace.path.shortcuts/shortcuts - all-workspace-shortcuts (->> (d/deep-merge path-shortcuts workspace-shortcuts) - (add-translation :sc) + (let [profile (mf/deref refs/profile) + custom-shortcuts (get-in profile [:props :custom-shortcuts]) + + path-shortcuts-custom (ds/apply-custom-overrides psc/shortcuts custom-shortcuts :path) + + workspace-shortcuts-custom (ds/apply-custom-overrides wsc/shortcuts custom-shortcuts :workspace) + + workspace-shortcuts (->> (d/deep-merge path-shortcuts-custom workspace-shortcuts-custom) + (ss/add-translation :sc) (into {})) - dashboard-shortcuts (->> app.main.data.dashboard.shortcuts/shortcuts - (add-translation :sc) + + dashboard-shortcuts-custom (ds/apply-custom-overrides dsc/shortcuts custom-shortcuts :dashboard) + dashboard-shortcuts (->> dashboard-shortcuts-custom + (ss/add-translation :sc) (into {})) - viewer-shortcuts (->> app.main.data.viewer.shortcuts/shortcuts - (add-translation :sc) + + viewer-shortcuts-custom (ds/apply-custom-overrides vsc/shortcuts custom-shortcuts :viewer) + viewer-shortcuts (->> viewer-shortcuts-custom + (ss/add-translation :sc) (into {})) - open-sections (mf/use-state [[1]]) - filter-term (mf/use-state "") + + open-sections* (mf/use-state [[:workspace]]) + open-sections (deref open-sections*) + filter-term* (mf/use-state "") + filter-term (deref filter-term*) 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 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) + match-any? (some #(matches-search % filter-term) all-item-names) manage-sections (fn [item] (fn [event] (dom/stop-propagation event) - (let [is-present? (some #(= % item) @open-sections) + (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)))) + (filterv (fn [element] (not= element item)) open-sections) + (conj open-sections item))] + (reset! open-sections* new-value)))) add-ids (fn [acc node] - (let [id (:id node) - addition (case (count id) - 1 id - 2 [[(first id)] id] - 3 [[(first id)] [(first id) (second id)]] - "default" nil)] - (if (= 1 (count addition)) - (conj acc addition) - (into [] (concat acc addition))))) + (let [id (:id node) + parents (when (> (count id) 1) + (mapv (fn [n] (vec (take n id))) + (range 1 (count id))))] + (into [] (concat acc parents)))) manage-section-on-search (fn [section term] @@ -468,23 +98,28 @@ manage-sections-on-search (fn [term] (if (= term "") - (reset! open-sections [[1]]) + (reset! open-sections* [[:workspace]]) (let [ids (set/union (manage-section-on-search :basics term) (manage-section-on-search :workspace term) (manage-section-on-search :dashboard term) (manage-section-on-search :viewer term))] - (reset! open-sections ids)))) + (reset! open-sections* ids)))) on-search-term-change-2 (mf/use-callback (fn [value] (manage-sections-on-search value) - (reset! filter-term value))) + (reset! filter-term* value))) + on-search-clear-click (mf/use-callback (fn [_] - (reset! open-sections [[1]]) - (reset! filter-term "")))] + (reset! open-sections* [[:workspace]]) + (reset! filter-term* ""))) + + go-to-edit-shortcuts + (mf/use-fn + #(st/emit! (rt/nav :settings-shortcuts {} {::rt/new-window true})))] [:div {:class (dm/str class " " (stl/css :shortcuts))} [:> panel-title* {:class (stl/css :shortcuts-title) @@ -494,7 +129,7 @@ [:div {:class (stl/css :search-field)} [:> search-bar* {:on-change on-search-term-change-2 :on-clear on-search-clear-click - :value @filter-term + :value filter-term :placeholder (tr "shortcuts.title") :icon-id i/search :auto-focus true}]] @@ -502,9 +137,20 @@ (if match-any? [:div {:class (stl/css :shortcuts-list)} (for [section all-shortcuts] - [:> shortcut-section* {:key (->> section second :id first) - :section section - :manage-sections manage-sections - :open-sections open-sections - :filter-term filter-term}])] - [:div {:class (stl/css :not-found)} (tr "shortcuts.not-found")])])) + (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? false + :custom-shortcuts custom-shortcuts}]))] + [:div {:class (stl/css :not-found)} (tr "shortcuts.not-found")]) + (when (contains? cf/flags :custom-shortcuts) + [:> bl/button-link* {:on-click go-to-edit-shortcuts + :label (tr "shortcuts.edit-on-settings") + :class (stl/css :edit-shortcuts-button) + :icon (mf/html + [:> icon* {:icon-id i/open-link + :size "m" + :aria-hidden true}])}])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss index d375082b1a..a2b1771b30 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.scss @@ -5,115 +5,63 @@ // Copyright (c) KALEIDOS INC Sucursal en España SL @use "refactor/common-refactor.scss" as deprecated; +@use "ds/typography.scss" as t; +@use "ds/_borders.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; .shortcuts { display: grid; grid-template-rows: auto auto 1fr; // TODO: Fix this once we start implementing the DS. - // We should not be doign these hardcoded calc's. - height: calc(100vh - #{deprecated.$s-60}); + // We should not be doing these hardcoded calc's. + block-size: calc(100vh - #{px2rem(60)}); } .search-field { - margin: deprecated.$s-16 deprecated.$s-12 deprecated.$s-4 deprecated.$s-12; + display: flex; + align-items: center; + gap: var(--sp-xs); + margin: var(--sp-l) var(--sp-m) var(--sp-xs) var(--sp-m); + + // The search bar should fill available space; the reset-all button sits at the end + & > :first-child { + flex: 1; + min-inline-size: 0; + } } .shortcuts-title { margin: var(--sp-s) var(--sp-s) 0 var(--sp-s); } -.section { - margin: 0; +.shortcuts-list { + @include t.use-typography("body-small"); + + display: flex; + flex-direction: column; + block-size: 100%; + padding: var(--sp-m); + overflow-y: auto; + color: var(--color-foreground-secondary); + min-block-size: px2rem(250); } .not-found { - @include deprecated.body-small-typography; + @include t.use-typography("body-small"); - color: var(--empty-message-foreground-color); - margin: deprecated.$s-12; + color: var(--color-foreground-secondary); + margin: var(--sp-m); } -.shortcuts-list { +.edit-shortcuts-button { + @include t.use-typography("headline-small"); + + width: fit-content; 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); - } - - .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; - } - } - } - } + align-items: center; + gap: var(--sp-xs); + justify-content: center; } diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 141986c4f2..d820f01aa2 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -479,7 +479,7 @@ (defn setup-shortcuts [path-editing? drawing-path? text-editing? grid-editing?] - (hooks/use-shortcuts ::workspace wsc/shortcuts) + (hooks/use-shortcuts ::workspace wsc/shortcuts :workspace) (mf/with-effect [] (.addEventListener js/window "keydown" wsc/on-display-guides-keydown) @@ -490,18 +490,20 @@ (cond grid-editing? (do - (st/emit! (dsc/push-shortcuts ::grid gsc/shortcuts)) + (st/emit! (dsc/push-shortcuts ::grid gsc/shortcuts :workspace + :merge-shortcuts :auto)) (fn [] (st/emit! (dsc/pop-shortcuts ::grid)))) (or drawing-path? path-editing?) (do - (st/emit! (dsc/push-shortcuts ::path psc/shortcuts)) + (st/emit! (dsc/push-shortcuts ::path psc/shortcuts :workspace + :merge-shortcuts :auto)) (fn [] (st/emit! (dsc/pop-shortcuts ::path)))) text-editing? (do - (st/emit! (dsc/push-shortcuts ::text tsc/shortcuts)) + (st/emit! (dsc/push-shortcuts ::text tsc/shortcuts :workspace)) (fn [] (st/emit! (dsc/pop-shortcuts ::text))))))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index ca2809cfba..e2406a959c 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -795,10 +795,8 @@ (defn trigger-download [filename blob] (let [uri (wapi/create-uri blob)] - (try - (trigger-download-uri filename (.-type ^js blob) uri) - (finally - (wapi/revoke-uri uri))))) + (trigger-download-uri filename (.-type ^js blob) uri) + (js/setTimeout #(wapi/revoke-uri uri) 1000))) (defn event "Create an instance of DOM Event" diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 9ea2a49c82..8882abbfa7 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -62,6 +62,7 @@ [frontend-tests.ui.layout-container-multiple-test] [frontend-tests.ui.measures-menu-props-test] [frontend-tests.ui.settings-password-schema-test] + [frontend-tests.ui.settings-shortcuts-test] [frontend-tests.util-clipboard-test] [frontend-tests.util-object-test] [frontend-tests.util-range-tree-test] @@ -140,6 +141,7 @@ 'frontend-tests.ui.layout-container-multiple-test 'frontend-tests.ui.measures-menu-props-test 'frontend-tests.ui.settings-password-schema-test + 'frontend-tests.ui.settings-shortcuts-test 'frontend-tests.util-clipboard-test 'frontend-tests.util-object-test 'frontend-tests.util-range-tree-test diff --git a/frontend/test/frontend_tests/ui/settings_shortcuts_test.cljs b/frontend/test/frontend_tests/ui/settings_shortcuts_test.cljs new file mode 100644 index 0000000000..0def5fb1c9 --- /dev/null +++ b/frontend/test/frontend_tests/ui/settings_shortcuts_test.cljs @@ -0,0 +1,180 @@ +(ns frontend-tests.ui.settings-shortcuts-test + (:require + [app.main.data.profile :as du] + [app.main.ui.settings.restore-shortcuts-modal :as restore-modal] + [app.main.ui.settings.shortcuts :as sut] + [app.main.ui.shortcuts :as ui-shortcuts] + [cljs.test :as t :include-macros true] + [clojure.string :as str])) + +(t/deftest validate-imported-shortcuts-accepts-valid-workspace + (t/is (= {:valid? true} + (sut/validate-imported-shortcuts + {:workspace {:escape "escape" + :increase-zoom "+" + :zoom-lense-decrease "alt+z"}})))) + +(t/deftest validate-imported-shortcuts-accepts-optional-contexts + (t/is (= {:valid? true} + (sut/validate-imported-shortcuts + {:workspace {:escape "escape"} + :dashboard {:toggle-theme "alt+m"} + :viewer {:next-frame "right"}})))) + +(t/deftest validate-imported-shortcuts-accepts-empty-string-command + (t/is (= {:valid? true} + (sut/validate-imported-shortcuts + {:workspace {:escape ""}})))) + +(t/deftest validate-imported-shortcuts-rejects-nil + (let [result (sut/validate-imported-shortcuts nil)] + (t/is (= false (:valid? result))) + (t/is (seq (:errors result))))) + +(t/deftest validate-imported-shortcuts-rejects-non-map + (t/is (= false (:valid? (sut/validate-imported-shortcuts "shortcuts")))) + (t/is (= false (:valid? (sut/validate-imported-shortcuts 42)))) + (t/is (= false (:valid? (sut/validate-imported-shortcuts [])))) + (t/is (= false (:valid? (sut/validate-imported-shortcuts true))))) + +(t/deftest validate-imported-shortcuts-accepts-missing-workspace + (t/is (= {:valid? true} + (sut/validate-imported-shortcuts {:dashboard {:toggle-theme "alt+m"}})))) + +(t/deftest validate-imported-shortcuts-rejects-non-map-context + (let [result (sut/validate-imported-shortcuts {:workspace "not-a-map"})] + (t/is (= false (:valid? result))) + (t/is (seq (:errors result))) + (t/is (some #(= "workspace" (:path %)) (:errors result))))) + +(t/deftest validate-imported-shortcuts-rejects-non-string-command + (let [result (sut/validate-imported-shortcuts {:workspace {:escape 123}})] + (t/is (= false (:valid? result))) + (t/is (seq (:errors result))) + (t/is (some #(= "workspace/escape" (:path %)) (:errors result))))) + +(t/deftest validate-imported-shortcuts-rejects-vector-command + (let [result (sut/validate-imported-shortcuts {:workspace {:escape ["ctrl+e"]}})] + (t/is (= false (:valid? result))) + (t/is (seq (:errors result))) + (t/is (some #(= "workspace/escape" (:path %)) (:errors result))))) + +(t/deftest validate-imported-shortcuts-rejects-unknown-top-level-key + (let [result (sut/validate-imported-shortcuts {:workspace {:escape "escape"} + :unknown-context {}})] + (t/is (= false (:valid? result))) + (t/is (seq (:errors result))) + (t/is (some #(str/includes? (:path %) "unknown-context") (:errors result))))) + +(t/deftest validate-imported-shortcuts-rejects-unknown-workspace-key + (let [result (sut/validate-imported-shortcuts {:workspace {:not-a-real-shortcut "x"}})] + (t/is (= false (:valid? result))) + (t/is (seq (:errors result))) + (t/is (some #(= "workspace/not-a-real-shortcut" (:path %)) (:errors result))))) + +(t/deftest validate-imported-shortcuts-rejects-unknown-optional-context-key + (let [result (sut/validate-imported-shortcuts {:viewer {:not-a-real-shortcut "x"}})] + (t/is (= false (:valid? result))) + (t/is (seq (:errors result))) + (t/is (some #(= "viewer/not-a-real-shortcut" (:path %)) (:errors result))))) + +(t/deftest validate-imported-shortcuts-never-throws + (t/is (= false (:valid? (sut/validate-imported-shortcuts (js/Date.))))) + (t/is (= false (:valid? (sut/validate-imported-shortcuts #{:workspace})))) + (t/is (= false (:valid? (sut/validate-imported-shortcuts (fn [])))))) + +(t/deftest import-custom-shortcuts-updates-multiple-contexts + (let [imported {:workspace {:add-comment "alt+c"} + :viewer {:select-all "ctrl+alt+a"}} + all-raw {:add-comment {:command "alt+c"} + :select-all {:command "ctrl+a"}}] + (with-redefs [du/update-profile-props identity] + (t/is (= {:custom-shortcuts {:workspace {:add-comment "alt+c"} + :viewer {:select-all "ctrl+alt+a"}}} + (ui-shortcuts/import-custom-shortcuts imported all-raw)))))) + +(t/deftest import-custom-shortcuts-preserves-unimported-contexts + (let [current {:workspace {:existing "ctrl+e"} + :dashboard {:toggle-theme "alt+m"} + :viewer {:select-all "ctrl+a"}} + imported {:workspace {:add-comment "alt+c"}} + all-raw {:add-comment {:command "alt+c"} + :existing {:command "ctrl+e"} + :toggle-theme {:command "alt+m"} + :select-all {:command "ctrl+a"}}] + (with-redefs [du/update-profile-props identity] + (t/is (= {:custom-shortcuts {:workspace {:add-comment "alt+c"} + :dashboard {:toggle-theme "alt+m"} + :viewer {:select-all "ctrl+a"}}} + (ui-shortcuts/import-custom-shortcuts + {:profile {:props {:custom-shortcuts current}} + :workspace (:workspace imported)} + all-raw)))))) + +(t/deftest import-custom-shortcuts-clears-context-when-empty + (let [current {:workspace {:existing "ctrl+e"}} + imported {:workspace {}} + all-raw {}] + (with-redefs [du/update-profile-props identity] + (t/is (= {:custom-shortcuts {:workspace {}}} + (ui-shortcuts/import-custom-shortcuts + {:profile {:props {:custom-shortcuts current}} + :workspace (:workspace imported)} + all-raw)))))) + +(t/deftest import-custom-shortcuts-disables-conflicting-default + (let [imported {:workspace {:select-all "ctrl+a"}} + all-raw {:select-all {:command "ctrl+a"} + :add-comment {:command "ctrl+a"}}] + (with-redefs [du/update-profile-props identity] + (t/is (= {:custom-shortcuts {:workspace {:select-all "ctrl+a" + :add-comment ""}}} + (ui-shortcuts/import-custom-shortcuts imported all-raw)))))) + +(t/deftest validate-imported-shortcuts-accepts-viewer-only-import + (t/is (= {:valid? true} + (sut/validate-imported-shortcuts + {:viewer {:next-frame "right" + :prev-frame "left"}})))) + +(t/deftest import-custom-shortcuts-disables-duplicate-imported-bindings + (let [imported {:workspace {:move-up "ctrl+up" + :move-to-top "ctrl+up"}} + all-raw {:move-up {:command "ctrl+shift+up"} + :move-to-top {:command "ctrl+shift+top"}}] + (with-redefs [du/update-profile-props identity] + (let [customs (get-in (ui-shortcuts/import-custom-shortcuts imported all-raw) + [:custom-shortcuts :workspace])] + (t/is (= "" (get customs :move-up)) + "First imported entry with duplicate command should be cleared") + (t/is (= "ctrl+up" (get customs :move-to-top)) + "Last imported entry with duplicate command should survive"))))) + +(t/deftest extract-shortcut-keys-uses-correct-context-override + (let [customs {:workspace {:undo "shift+z" + :move-nodes "shift+m"} + :dashboard {:toggle-theme "alt+t"} + :viewer {:next-frame "right"}}] + (t/is (= "shift+z" (nth (restore-modal/extract-shortcut-keys :undo customs :workspace) 3)) + "Workspace override should be used for :undo") + (t/is (= "alt+t" (nth (restore-modal/extract-shortcut-keys :toggle-theme customs :dashboard) 3)) + "Dashboard override should be used for :toggle-theme") + (t/is (= "right" (nth (restore-modal/extract-shortcut-keys :next-frame customs :viewer) 3)) + "Viewer override should be used for :next-frame") + (t/is (= "shift+m" (nth (restore-modal/extract-shortcut-keys :move-nodes customs :workspace) 3)) + "Workspace override should be used for path-specific :move-nodes"))) + +(t/deftest extract-shortcut-keys-returns-default-for-each-context + (let [result (restore-modal/extract-shortcut-keys :move-nodes {} :workspace)] + (t/is (string? (nth result 3)) + "Should return default command string for :move-nodes via :workspace context") + (t/is (not= "" (nth result 3)) + "Default command should not be empty")) + (let [result (restore-modal/extract-shortcut-keys :toggle-theme {} :dashboard)] + (t/is (string? (nth result 3)) + "Should return default command string for :toggle-theme in :dashboard context") + (t/is (not= "" (nth result 3)) + "Default command should not be empty")) + (let [result (restore-modal/extract-shortcut-keys :next-frame {} :viewer)] + (t/is (nth result 3) + "Should return a default command for :next-frame in :viewer context"))) diff --git a/frontend/translations/ar.po b/frontend/translations/ar.po index 218976a3f6..6b8004563c 100644 --- a/frontend/translations/ar.po +++ b/frontend/translations/ar.po @@ -2113,17 +2113,10 @@ msgstr "مساحة العمل" msgid "shortcut-subsection.alignment" msgstr "محاذاة" -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59 -msgid "shortcut-subsection.edit" -msgstr "عدّل" - #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" +msgid "shortcut-subsection.generic" msgstr "عام" -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" -msgstr "عام" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 msgid "shortcut-subsection.main-menu" diff --git a/frontend/translations/ca.po b/frontend/translations/ca.po index fd724a3a67..125f54d1ce 100644 --- a/frontend/translations/ca.po +++ b/frontend/translations/ca.po @@ -2002,16 +2002,8 @@ msgstr "Espai de treball" msgid "shortcut-subsection.alignment" msgstr "Alineació" -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59 -msgid "shortcut-subsection.edit" -msgstr "Edició" - #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Genèric" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Genèric" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/cs.po b/frontend/translations/cs.po index 78f269bf98..396a3ae50e 100644 --- a/frontend/translations/cs.po +++ b/frontend/translations/cs.po @@ -3242,16 +3242,8 @@ msgstr "Pracovní plocha" msgid "shortcut-subsection.alignment" msgstr "Zarovnání" -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59 -msgid "shortcut-subsection.edit" -msgstr "Upravit" - #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Obecný" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Obecný" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/de.po b/frontend/translations/de.po index b3b060cf94..18149745bd 100644 --- a/frontend/translations/de.po +++ b/frontend/translations/de.po @@ -4143,16 +4143,8 @@ msgstr "Arbeitsbereich" msgid "shortcut-subsection.alignment" msgstr "Ausrichtung" -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59 -msgid "shortcut-subsection.edit" -msgstr "Bearbeiten" - #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Allgemein" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Allgemein" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 82e3f9a11d..25e4534110 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2873,6 +2873,11 @@ msgstr "Create new team" msgid "labels.create-team.placeholder" msgstr "Enter new team name" + +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "labels.current" +msgstr "Current" + #, unused msgid "labels.custom-fonts" msgstr "Custom fonts" @@ -2881,6 +2886,10 @@ msgstr "Custom fonts" msgid "labels.dashboard" msgstr "Dashboard" +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "labels.default" +msgstr "Default" + #: src/app/main/ui/dashboard/file_menu.cljs:337, src/app/main/ui/dashboard/fonts.cljs:286, src/app/main/ui/dashboard/fonts.cljs:363, src/app/main/ui/dashboard/fonts.cljs:397, src/app/main/ui/dashboard/project_menu.cljs:115, src/app/main/ui/dashboard/team.cljs:1379, src/app/main/ui/settings/integrations.cljs:389, src/app/main/ui/settings/profile.cljs:117, src/app/main/ui/settings/profile.cljs:128, src/app/main/ui/settings/profile.cljs:129, src/app/main/ui/workspace/sidebar/assets/common.cljs:231, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:223, src/app/main/ui/workspace/sidebar/versions.cljs:258, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:353, src/app/main/ui/workspace/tokens/management/node_context_menu.cljs:111, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:378 msgid "labels.delete" msgstr "Delete" @@ -5099,16 +5108,12 @@ msgstr "Workspace" msgid "shortcut-subsection.alignment" msgstr "Alignment" -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:59 +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 msgid "shortcut-subsection.edit" msgstr "Edit" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Generic" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Generic" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 @@ -5626,11 +5631,79 @@ msgstr "Stop measurement" msgid "shortcuts.thumbnail-set" msgstr "Set thumbnails" +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.page" +msgstr "Shortcuts page" + #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:491, src/app/main/ui/workspace/sidebar/shortcuts.cljs:498 msgid "shortcuts.title" msgstr "Keyboard shortcuts" -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185 +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.reset" +msgstr "Reset to default" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.disable" +msgstr "Disable shortcut" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.disabled-shortcut" +msgstr "Disabled shortcut" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.overwritten-by" +msgstr "This shortcut has been reassigned to %s" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.no-personalized" +msgstr "Head to All to start customizing your shortcuts." + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.no-disabled" +msgstr "There are not disabled shortcuts" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.reload-hint" +msgstr "After editing shortcuts, reload any other open Penpot tabs to apply the changes." + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.no-shortcuts" +msgstr "There are no shortcuts that match your search" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.personalized" +msgstr "Personalized" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.disabled" +msgstr "Disabled" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.draw-arrow" +msgstr "Arrow" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.draw-line" +msgstr "Line" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.import-export" +msgstr "Import/Export" + +#: src/app/main/ui/shortcuts.cljs +msgid "shortcuts.key-combo" +msgstr "Press the key combination" + +#: src/app/main/ui/shortcuts.cljs +msgid "shortcuts.edit-modal.conflict" +msgstr "Combination assigned to \"%s\". Saving will remove it there." + +#: src/app/main/ui/shortcuts.cljs +msgid "shortcuts.edit-modal.success" +msgstr "This key combination is available and can be assigned without conflicts." + +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:182 msgid "shortcuts.toggle-alignment" msgstr "Toggle dynamic alignment" @@ -5642,6 +5715,10 @@ msgstr "Toggle assets" msgid "shortcuts.toggle-colorpalette" msgstr "Toggle color palette" +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs +msgid "shortcuts.toggle-comments-visibility" +msgstr "Toggle comments visibility" + #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:188 msgid "shortcuts.toggle-focus-mode" msgstr "Toggle focus mode" @@ -5738,6 +5815,26 @@ msgstr "Zoom lense increase" msgid "shortcuts.zoom-selected" msgstr "Zoom to selected" +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs +msgid "shortcuts.edit-on-settings" +msgstr "Customize on settings" + +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "restore-shortcuts.modal-title" +msgstr "Restore to default configuration" + +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "restore-shortcuts.modal-text" +msgstr "The following shortcuts will be restored to their default values." + +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "restore-shortcuts.acction" +msgstr "Acction" + +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "restore-shortcuts.restore" +msgstr "Restore" + #: src/app/main/ui/dashboard/subscription.cljs:191 msgid "subscription.banner.create-org-info" msgstr "" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ca27370228..85ddaf4897 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -2808,6 +2808,10 @@ msgstr "Crea un nuevo equipo" msgid "labels.create-team.placeholder" msgstr "Introduce un nuevo nombre de equipo" +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "labels.current" +msgstr "Valor Actual" + #, unused msgid "labels.custom-fonts" msgstr "Fuentes personalizadas" @@ -2816,6 +2820,11 @@ msgstr "Fuentes personalizadas" msgid "labels.dashboard" msgstr "Panel" +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "labels.default" +msgstr "Valor por defecto" + + #: src/app/main/ui/dashboard/file_menu.cljs:337, src/app/main/ui/dashboard/fonts.cljs:286, src/app/main/ui/dashboard/fonts.cljs:363, src/app/main/ui/dashboard/fonts.cljs:397, src/app/main/ui/dashboard/project_menu.cljs:115, src/app/main/ui/dashboard/team.cljs:1379, src/app/main/ui/settings/integrations.cljs:389, src/app/main/ui/settings/profile.cljs:117, src/app/main/ui/settings/profile.cljs:128, src/app/main/ui/settings/profile.cljs:129, src/app/main/ui/workspace/sidebar/assets/common.cljs:231, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:223, src/app/main/ui/workspace/sidebar/versions.cljs:258, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:353, src/app/main/ui/workspace/tokens/management/node_context_menu.cljs:111, src/app/main/ui/workspace/tokens/sets/context_menu.cljs:66, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:378 msgid "labels.delete" msgstr "Borrar" @@ -4943,6 +4952,7 @@ msgstr "Seleccionar elementos que usan este estilo" msgid "shortcut-section.basics" msgstr "Básicos" + #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:418 msgid "shortcut-section.dashboard" msgstr "Panel" @@ -4965,11 +4975,7 @@ msgid "shortcut-subsection.edit" msgstr "Editar" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Genérico" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Genérico" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 @@ -5475,10 +5481,78 @@ msgstr "Terminar medida" msgid "shortcuts.thumbnail-set" msgstr "Activar miniaturas" +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.page" +msgstr "Página de atajos" + #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:491, src/app/main/ui/workspace/sidebar/shortcuts.cljs:498 msgid "shortcuts.title" msgstr "Atajos de teclado" +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.reset" +msgstr "Restablecer valor por defecto" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.disable" +msgstr "Deshabilitar atajo" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.disabled-shortcut" +msgstr "Atajo deshabilitado" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.overwritten-by" +msgstr "Este atajo ha sido reasignado a %s" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.no-personalized" +msgstr "Dirígete a TODO para personalizar tus atajos." + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.no-disabled" +msgstr "No hay atajos deshabilitados." + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.reload-hint" +msgstr "Después de editar un atajo, recarga cualquier pestaña de Penpot para aplicar los cambios." + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.no-shortcuts" +msgstr "No hay atajos que coincidan con tu búsqueda." + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.personalized" +msgstr "Personalizados" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.disabled" +msgstr "Deshabilitado" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.draw-arrow" +msgstr "Flecha" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.draw-line" +msgstr "Linea" + +#: src/app/main/ui/settings/shortcuts.cljs +msgid "shortcuts.import-export" +msgstr "Importar/Exportar" + +#: src/app/main/ui/shortcuts.cljs +msgid "shortcuts.key-combo" +msgstr "Pulsa la combinación de teclas deseada" + +#: src/app/main/ui/shortcuts.cljs +msgid "shortcuts.edit-modal.conflict" +msgstr "Combinación usada en \"%s\". Guardar este atajo hará que se elimine allí." + +#: src/app/main/ui/shortcuts.cljs +msgid "shortcuts.edit-modal.success" +msgstr "Esta combinación de teclas está disponible y puede asignarse sin conflictos." + #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185 msgid "shortcuts.toggle-alignment" msgstr "Alternar alineación" @@ -5491,6 +5565,10 @@ msgstr "Mostrar/ocultar recursos" msgid "shortcuts.toggle-colorpalette" msgstr "Mostrar/ocultar paleta de colores" +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs +msgid "shortcuts.toggle-comments-visibility" +msgstr "Activar/desactivar visibilidad de comentarios" + #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:188 msgid "shortcuts.toggle-focus-mode" msgstr "Mostrar/ocultar focus mode" @@ -5587,6 +5665,26 @@ msgstr "Incrementar zoom a objetivo" msgid "shortcuts.zoom-selected" msgstr "Zoom a selección" +#: src/app/main/ui/workspace/sidebar/shortcuts.cljs +msgid "shortcuts.edit-on-settings" +msgstr "Personalízalos en ajustes" + +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "restore-shortcuts.modal-title" +msgstr "Restaurar la configuración predeterminada" + +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "restore-shortcuts.modal-text" +msgstr "Los siguientes atajos de teclado se restaurarán a sus valores predeterminados." + +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "restore-shortcuts.acction" +msgstr "Acción" + +#: src/app/main/ui/settings/restore_shortcuts_modal.cljs +msgid "restore-shortcuts.restore" +msgstr "Restaurar" + #: src/app/main/ui/dashboard/subscription.cljs:191 msgid "subscription.banner.create-org-info" msgstr "" diff --git a/frontend/translations/eu.po b/frontend/translations/eu.po index 4b4b47d0fe..873a38f5d8 100644 --- a/frontend/translations/eu.po +++ b/frontend/translations/eu.po @@ -2294,11 +2294,7 @@ msgid "shortcut-subsection.edit" msgstr "Editatu" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Orokorra" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Orokorra" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/fr.po b/frontend/translations/fr.po index 78b9696558..98044073d1 100644 --- a/frontend/translations/fr.po +++ b/frontend/translations/fr.po @@ -4178,11 +4178,7 @@ msgid "shortcut-subsection.edit" msgstr "Modifier" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Générique" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Générique" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/fr_CA.po b/frontend/translations/fr_CA.po index 720ad09a06..5e32136781 100644 --- a/frontend/translations/fr_CA.po +++ b/frontend/translations/fr_CA.po @@ -5076,11 +5076,7 @@ msgid "shortcut-subsection.edit" msgstr "Édition" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Général" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Général" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/gl.po b/frontend/translations/gl.po index fcca7a664a..bcba973c12 100644 --- a/frontend/translations/gl.po +++ b/frontend/translations/gl.po @@ -1145,11 +1145,7 @@ msgid "shortcut-subsection.edit" msgstr "Editar" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Xenérico" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Xenérico" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:64 diff --git a/frontend/translations/ha.po b/frontend/translations/ha.po index ca57b49e82..49c69fffaf 100644 --- a/frontend/translations/ha.po +++ b/frontend/translations/ha.po @@ -2436,11 +2436,7 @@ msgid "shortcut-subsection.edit" msgstr "Tace" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "gamayya" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "gamayya" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/he.po b/frontend/translations/he.po index 5e84585b67..b1240c366e 100644 --- a/frontend/translations/he.po +++ b/frontend/translations/he.po @@ -4079,11 +4079,7 @@ msgid "shortcut-subsection.edit" msgstr "עריכה" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "כללי" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "כללי" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/hi.po b/frontend/translations/hi.po index 29f6f44f30..b0eb71cc0a 100644 --- a/frontend/translations/hi.po +++ b/frontend/translations/hi.po @@ -4032,11 +4032,7 @@ msgid "shortcut-subsection.edit" msgstr "संपादन" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "सार्वभौमिक" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "सार्वभौमिक" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/hr.po b/frontend/translations/hr.po index 8b46f9df1b..df91effe6c 100644 --- a/frontend/translations/hr.po +++ b/frontend/translations/hr.po @@ -3246,11 +3246,7 @@ msgid "shortcut-subsection.edit" msgstr "Uredi" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Generičko" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Generičko" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/id.po b/frontend/translations/id.po index de7f6baf49..8947a3469d 100644 --- a/frontend/translations/id.po +++ b/frontend/translations/id.po @@ -3467,11 +3467,7 @@ msgid "shortcut-subsection.edit" msgstr "Sunting" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Generik" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Generik" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/it.po b/frontend/translations/it.po index f4f0a13bb2..78c40e6930 100644 --- a/frontend/translations/it.po +++ b/frontend/translations/it.po @@ -4588,11 +4588,7 @@ msgid "shortcut-subsection.edit" msgstr "Modifica" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Generico" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Generico" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/ko.po b/frontend/translations/ko.po index c2af51bf67..f7bb50c9e1 100644 --- a/frontend/translations/ko.po +++ b/frontend/translations/ko.po @@ -3859,11 +3859,7 @@ msgid "shortcut-subsection.edit" msgstr "편집" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "일반" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "일반" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/lv.po b/frontend/translations/lv.po index 2e5e4baab6..9d0295c905 100644 --- a/frontend/translations/lv.po +++ b/frontend/translations/lv.po @@ -3786,11 +3786,7 @@ msgid "shortcut-subsection.edit" msgstr "Labot" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Vispārējs" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Vispārējs" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/ms.po b/frontend/translations/ms.po index d15d2f2adc..107b67868f 100644 --- a/frontend/translations/ms.po +++ b/frontend/translations/ms.po @@ -2550,11 +2550,7 @@ msgid "shortcut-subsection.edit" msgstr "Sunting" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Generik" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Generik" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/nl.po b/frontend/translations/nl.po index 087c65a440..531c4e4a51 100644 --- a/frontend/translations/nl.po +++ b/frontend/translations/nl.po @@ -4186,11 +4186,7 @@ msgid "shortcut-subsection.edit" msgstr "Bewerken" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Algemeen" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Algemeen" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/pl.po b/frontend/translations/pl.po index 982243749b..d6aed9eb0b 100644 --- a/frontend/translations/pl.po +++ b/frontend/translations/pl.po @@ -2286,11 +2286,7 @@ msgid "shortcut-subsection.edit" msgstr "Edytuj" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Ogólny" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Ogólny" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/pt_BR.po b/frontend/translations/pt_BR.po index 9563253ed1..100b1a0426 100644 --- a/frontend/translations/pt_BR.po +++ b/frontend/translations/pt_BR.po @@ -3042,11 +3042,7 @@ msgid "shortcut-subsection.edit" msgstr "Editar" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Geral" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Geral" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/pt_PT.po b/frontend/translations/pt_PT.po index 0e55c63192..5dbf031c04 100644 --- a/frontend/translations/pt_PT.po +++ b/frontend/translations/pt_PT.po @@ -3309,11 +3309,7 @@ msgid "shortcut-subsection.edit" msgstr "Editar" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Genérico" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Genérico" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/ro.po b/frontend/translations/ro.po index 6c341e704f..b224b764c9 100644 --- a/frontend/translations/ro.po +++ b/frontend/translations/ro.po @@ -3878,11 +3878,7 @@ msgid "shortcut-subsection.edit" msgstr "Editează" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Generic" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Generic" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/ru.po b/frontend/translations/ru.po index a68e44ccb9..828afc1571 100644 --- a/frontend/translations/ru.po +++ b/frontend/translations/ru.po @@ -3905,11 +3905,7 @@ msgid "shortcut-subsection.edit" msgstr "Редактировать" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Общее" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Общее" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/sr.po b/frontend/translations/sr.po index 3f6cde261a..333abe38cd 100644 --- a/frontend/translations/sr.po +++ b/frontend/translations/sr.po @@ -2812,11 +2812,7 @@ msgid "shortcut-subsection.edit" msgstr "Уреди" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Опште" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Опште" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/sv.po b/frontend/translations/sv.po index fe624b7c13..728ff5a39d 100644 --- a/frontend/translations/sv.po +++ b/frontend/translations/sv.po @@ -5040,11 +5040,7 @@ msgid "shortcut-subsection.edit" msgstr "Redigera" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Allmän" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Allmän" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/tr.po b/frontend/translations/tr.po index 11d791a4a7..c61c0ecfa7 100644 --- a/frontend/translations/tr.po +++ b/frontend/translations/tr.po @@ -5042,11 +5042,7 @@ msgid "shortcut-subsection.edit" msgstr "Düzenle" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Genel" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Genel" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/ukr_UA.po b/frontend/translations/ukr_UA.po index 93b48db66c..f75760968f 100644 --- a/frontend/translations/ukr_UA.po +++ b/frontend/translations/ukr_UA.po @@ -4113,11 +4113,7 @@ msgid "shortcut-subsection.edit" msgstr "Редагувати" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "Загальні" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "Загальні" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/yo.po b/frontend/translations/yo.po index 90b330b8d2..e227f80d4f 100644 --- a/frontend/translations/yo.po +++ b/frontend/translations/yo.po @@ -2318,10 +2318,6 @@ msgstr "Titete" msgid "shortcut-subsection.edit" msgstr "Sàtunkọ" -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" -msgstr "àbùdá" - #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 msgid "shortcut-subsection.main-menu" msgstr "Akojọ ólórì aṣyn" diff --git a/frontend/translations/zh_CN.po b/frontend/translations/zh_CN.po index d69e418790..666e806f4a 100644 --- a/frontend/translations/zh_CN.po +++ b/frontend/translations/zh_CN.po @@ -3502,11 +3502,7 @@ msgid "shortcut-subsection.edit" msgstr "编辑" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" -msgstr "通用" - -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" +msgid "shortcut-subsection.generic" msgstr "通用" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 diff --git a/frontend/translations/zh_Hant.po b/frontend/translations/zh_Hant.po index 621f8b1398..30337f9087 100644 --- a/frontend/translations/zh_Hant.po +++ b/frontend/translations/zh_Hant.po @@ -3081,12 +3081,9 @@ msgid "shortcut-subsection.edit" msgstr "編輯" #: src/app/main/ui/workspace/sidebar/shortcuts.cljs:60 -msgid "shortcut-subsection.general-dashboard" +msgid "shortcut-subsection.generic" msgstr "一般" -#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:61 -msgid "shortcut-subsection.general-viewer" -msgstr "一般" #: src/app/main/ui/workspace/main_menu.cljs:1005, src/app/main/ui/workspace/sidebar/shortcuts.cljs:62 msgid "shortcut-subsection.main-menu"