From 433d812b9d8fdc7669aedafa2a951c18bb224a9a Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 17 May 2026 14:19:37 +0300 Subject: [PATCH] refactor(vue): format command shortcuts by platform --- packages/vue/src/editor/commands/actions.ts | 3 +- packages/vue/src/editor/commands/registry.ts | 28 ++++----- packages/vue/src/editor/commands/shortcut.ts | 57 +++++++++++++++++++ packages/vue/src/index.ts | 2 + src/components/CanvasMenu.vue | 13 +++-- .../vue/editor/commands/shortcut.test.ts | 25 ++++++++ 6 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 packages/vue/src/editor/commands/shortcut.ts create mode 100644 tests/engine/vue/editor/commands/shortcut.test.ts diff --git a/packages/vue/src/editor/commands/actions.ts b/packages/vue/src/editor/commands/actions.ts index 2fc147b28..e782540c1 100644 --- a/packages/vue/src/editor/commands/actions.ts +++ b/packages/vue/src/editor/commands/actions.ts @@ -1,4 +1,5 @@ import { editorCommandMetadata } from './registry' +import { formatShortcut } from './shortcut' import type { EditorCommand, EditorCommandId, EditorCommandMenuItem } from './types' export function createEditorCommandActions(commands: Record) { @@ -19,7 +20,7 @@ export function createEditorCommandActions(commands: Record> = { + mac: { + MOD: '⌘', + SHIFT: '⇧', + ALT: '⌥', + CTRL: '⌃' + }, + windows: { + MOD: 'Ctrl', + SHIFT: 'Shift', + ALT: 'Alt', + CTRL: 'Ctrl' + }, + linux: { + MOD: 'Ctrl', + SHIFT: 'Shift', + ALT: 'Alt', + CTRL: 'Ctrl' + } +} + +const MAC_MODIFIER_ORDER = ['CTRL', 'ALT', 'SHIFT', 'MOD'] +const STANDARD_MODIFIER_ORDER = ['MOD', 'CTRL', 'ALT', 'SHIFT'] + +export function shortcutPlatform(userAgent = navigator.userAgent): ShortcutPlatform { + if (/Mac|iPhone|iPad|iPod/u.test(userAgent)) return 'mac' + if (/Win/u.test(userAgent)) return 'windows' + return 'linux' +} + +function sortModifiers(modifiers: string[], platform: ShortcutPlatform): string[] { + const order = platform === 'mac' ? MAC_MODIFIER_ORDER : STANDARD_MODIFIER_ORDER + return [...modifiers].sort((a, b) => order.indexOf(a) - order.indexOf(b)) +} + +export function formatShortcut( + shortcut: string | undefined, + platform = shortcutPlatform() +): string | undefined { + if (!shortcut) return undefined + + return shortcut + .split(' ') + .map((combo) => { + const parts = combo.split('+').filter(Boolean) + const modifiers = parts.filter((part) => part in MODIFIER_DISPLAY[platform]) + const keys = parts.filter((part) => !(part in MODIFIER_DISPLAY[platform])) + const formattedModifiers = sortModifiers(modifiers, platform).map( + (part) => MODIFIER_DISPLAY[platform][part] + ) + const separator = platform === 'mac' ? '' : '+' + return [...formattedModifiers, ...keys].join(separator) + }) + .join(' ') +} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index f9d235c51..04ae70d3e 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -36,7 +36,9 @@ export { useSelectionCapabilities } from '#vue/editor/selection-capabilities/use /** Command and menu composition helpers. */ export { useEditorCommands } from '#vue/editor/commands/use' export { EDITOR_COMMAND_METADATA, editorCommandMetadata } from '#vue/editor/commands/registry' +export { formatShortcut, shortcutPlatform } from '#vue/editor/commands/shortcut' export type { EditorCommandMetadata } from '#vue/editor/commands/registry' +export type { ShortcutPlatform } from '#vue/editor/commands/shortcut' export type { EditorCommand, EditorCommandId } from '#vue/editor/commands/use' export { useMenuModel } from '#vue/editor/menu-model/use' export type { MenuActionNode, MenuEntry, MenuSeparatorNode } from '#vue/editor/menu-model/use' diff --git a/src/components/CanvasMenu.vue b/src/components/CanvasMenu.vue index 70b4900ee..9fbb48dbd 100644 --- a/src/components/CanvasMenu.vue +++ b/src/components/CanvasMenu.vue @@ -14,7 +14,8 @@ import { useI18n, useMenuModel, useSelectionState, - editorCommandMetadata + editorCommandMetadata, + formatShortcut } from '@open-pencil/vue' import type { EditorCommandId } from '@open-pencil/vue' @@ -62,7 +63,7 @@ function contextCommandTestId(id: EditorCommandId | undefined): string | undefin @select="execCommand('copy')" > {{ t.copy }}⌘C + >{{ formatShortcut('MOD+C') }} {{ t.cut }}⌘X + >{{ formatShortcut('MOD+X') }} {{ t.pasteHere }}⌘V + >{{ formatShortcut('MOD+V') }} - Duplicate⌘D + Duplicate{{ formatShortcut('MOD+D') }} {{ t.copyAsPNG }}⇧⌘C + >{{ formatShortcut('MOD+SHIFT+C') }} { + test('formats shortcuts for macOS', () => { + expect(formatShortcut('MOD+D', 'mac')).toBe('⌘D') + expect(formatShortcut('MOD+SHIFT+H', 'mac')).toBe('⇧⌘H') + expect(formatShortcut('MOD+ALT+K', 'mac')).toBe('⌥⌘K') + expect(formatShortcut('SHIFT+A', 'mac')).toBe('⇧A') + }) + + test('formats shortcuts for Windows and Linux', () => { + expect(formatShortcut('MOD+D', 'windows')).toBe('Ctrl+D') + expect(formatShortcut('MOD+SHIFT+H', 'windows')).toBe('Ctrl+Shift+H') + expect(formatShortcut('MOD+ALT+K', 'linux')).toBe('Ctrl+Alt+K') + expect(formatShortcut('SHIFT+A', 'linux')).toBe('Shift+A') + }) + + test('detects platform from user agent', () => { + expect(shortcutPlatform('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)')).toBe('mac') + expect(shortcutPlatform('Mozilla/5.0 (Windows NT 10.0; Win64; x64)')).toBe('windows') + expect(shortcutPlatform('Mozilla/5.0 (X11; Linux x86_64)')).toBe('linux') + }) +})