🎉 Add customizable shortcuts (#10237)

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
This commit is contained in:
Eva Marco 2026-07-24 10:36:32 +02:00 committed by GitHub
parent caacd482af
commit 5de06e8f77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
87 changed files with 3950 additions and 878 deletions

View File

@ -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"}

View File

@ -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))

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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;

View File

@ -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);
});
});

View File

@ -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

View File

@ -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)))))

View File

@ -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)))))

View File

@ -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]

View File

@ -68,4 +68,4 @@
(defn get-tooltip [shortcut]
(assert (contains? shortcuts shortcut) (str shortcut))
(get-in shortcuts [shortcut :tooltip]))
(get-in shortcuts [shortcut :tooltip]))

View File

@ -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]))))

View File

@ -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"})}})

View File

@ -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))

View File

@ -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*

View File

@ -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);
}

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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"}

View File

@ -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)
[:*

View File

@ -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]))

View File

@ -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]]))

View File

@ -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]

View File

@ -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]

View File

@ -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}

View File

@ -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]]]))

View File

@ -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?

View File

@ -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}])]]))

View File

@ -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*]]])

View File

@ -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*]]])

View File

@ -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")]]]]]))

View File

@ -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);
}

View File

@ -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}]]]]]))

View File

@ -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);
}

View File

@ -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}]])

View File

@ -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")]

View File

@ -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}]]))

View File

@ -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);
}

View File

@ -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))

View File

@ -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)

View File

@ -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*

View File

@ -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);
}

View File

@ -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

View File

@ -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);
}

View File

@ -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}

View File

@ -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]]))

View File

@ -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))))

View File

@ -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}])}])]))

View File

@ -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;
}

View File

@ -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)))))))

View File

@ -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"

View File

@ -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

View File

@ -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")))

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"