fix(ui): format shortcut tooltips by platform

This commit is contained in:
Danila Poyarkov 2026-05-17 14:31:29 +03:00
parent 8bd9abac15
commit 883601f548
4 changed files with 34 additions and 13 deletions

View file

@ -478,6 +478,26 @@ const noPropertyPanelInternalsOutsidePanel = createImportRule(
}
)
const MACOS_MODIFIER_GLYPH_PATTERN = /[]/u
const noHardcodedMacOSShortcutGlyphs = createTextRule(
'open-pencil/no-hardcoded-macos-shortcut-glyphs',
(sourceRel, content) => {
if (!sourceRel.endsWith('.vue')) return []
const diagnostics: Array<{ message: string; line?: number; column?: number }> = []
for (const match of content.matchAll(MACOS_MODIFIER_GLYPH_PATTERN)) {
const before = content.slice(0, match.index)
const lines = before.split('\n')
diagnostics.push({
message: 'Use formatShortcut() instead of hardcoding macOS-only modifier glyphs.',
line: lines.length,
column: lines.at(-1)?.length ?? 0
})
}
return diagnostics
}
)
const SHORTCUT_LABEL_PATTERN = /(?:Shift|Ctrl|Alt|Option|Cmd|Command||||)\s*[+)\w]/u
const noShortcutTextInLabels = createTextRule(
@ -546,6 +566,7 @@ export const openPencilArchitecturePlugin = {
noAppImportsInSharedUi,
noPropertyPanelInternalsOutsidePanel,
noShortcutTextInLabels,
noHardcodedMacOSShortcutGlyphs,
noUiImportsInCore
]
}

View file

@ -9,7 +9,7 @@ import {
} from 'reka-ui'
import { nextTick, ref, watch } from 'vue'
import { useEditorCommands, useI18n } from '@open-pencil/vue'
import { useEditorCommands, useI18n, formatShortcut } from '@open-pencil/vue'
import { menuItem, useMenuUI } from '@/components/ui/menu'
import { useEditorStore } from '@/app/editor/active-store'
@ -69,7 +69,7 @@ function zoomOut() {
const ZOOM_PRESETS: ReadonlyArray<{ label: string; level: number; shortcut?: string }> = [
{ label: '50%', level: 0.5 },
{ label: '100%', level: 1, shortcut: '0' },
{ label: '100%', level: 1, shortcut: 'MOD+0' },
{ label: '200%', level: 2 }
]
@ -126,15 +126,15 @@ watch(open, (v) => {
<DropdownMenuItem :class="itemCls" @select="zoomIn">
<span class="flex-1">{{ menuText.zoomIn }}</span>
<span class="text-[11px] text-muted">+</span>
<span class="text-[11px] text-muted">{{ formatShortcut('MOD++') }}</span>
</DropdownMenuItem>
<DropdownMenuItem :class="itemCls" @select="zoomOut">
<span class="flex-1">{{ menuText.zoomOut }}</span>
<span class="text-[11px] text-muted"></span>
<span class="text-[11px] text-muted">{{ formatShortcut('MOD+') }}</span>
</DropdownMenuItem>
<DropdownMenuItem :class="itemCls" @select="getCommand('view.zoomFit').run()">
<span class="flex-1">{{ commands.zoomToFit }}</span>
<span class="text-[11px] text-muted">1</span>
<span class="text-[11px] text-muted">{{ formatShortcut('SHIFT+1') }}</span>
</DropdownMenuItem>
<DropdownMenuItem
v-for="preset in ZOOM_PRESETS"
@ -144,7 +144,7 @@ watch(open, (v) => {
>
<icon-lucide-check v-if="isActivePreset(preset.level)" class="absolute left-2 size-3.5" />
<span class="flex-1">{{ preset.label }}</span>
<span v-if="preset.shortcut" class="text-[11px] text-muted">{{ preset.shortcut }}</span>
<span v-if="preset.shortcut" class="text-[11px] text-muted">{{ formatShortcut(preset.shortcut) }}</span>
</DropdownMenuItem>
<DropdownMenuSeparator :class="menuCls.separator" />

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ToggleGroupItem, ToggleGroupRoot } from 'reka-ui'
import { TypographyControlsRoot, useI18n } from '@open-pencil/vue'
import { TypographyControlsRoot, useI18n, formatShortcut } from '@open-pencil/vue'
import FontPicker from '@/components/FontPicker.vue'
import FontSettingsPopover from '@/components/FontSettings/FontSettingsPopover.vue'
@ -11,7 +11,7 @@ import Tip from '@/components/ui/Tip.vue'
import { useSectionUI } from '@/components/ui/section'
import { loadFont } from '@/app/editor/fonts'
const { panels } = useI18n()
const { panels, menu } = useI18n()
const sectionCls = useSectionUI()
const fontLoader = { load: loadFont }
</script>
@ -125,7 +125,7 @@ const fontLoader = { load: loadFont }
</ToggleGroupItem>
</ToggleGroupRoot>
<div class="flex gap-0.5">
<Tip label="Bold (⌘B)">
<Tip :label="`${menu.bold} (${formatShortcut('MOD+B')})`">
<button
data-test-id="typography-bold-button"
class="flex cursor-pointer items-center justify-center rounded border border-border bg-input px-2 py-1 font-bold text-muted hover:bg-hover hover:text-surface data-[state=on]:border-accent data-[state=on]:bg-accent data-[state=on]:text-white"
@ -135,7 +135,7 @@ const fontLoader = { load: loadFont }
<icon-lucide-bold class="size-3.5" />
</button>
</Tip>
<Tip label="Italic (⌘I)">
<Tip :label="`${menu.italic} (${formatShortcut('MOD+I')})`">
<button
class="flex cursor-pointer items-center justify-center rounded border border-border bg-input px-2 py-1 text-muted hover:bg-hover hover:text-surface data-[state=on]:border-accent data-[state=on]:bg-accent data-[state=on]:text-white"
:data-state="ctx.activeFormatting.value.includes('italic') ? 'on' : 'off'"
@ -144,7 +144,7 @@ const fontLoader = { load: loadFont }
<icon-lucide-italic class="size-3.5" />
</button>
</Tip>
<Tip label="Underline (⌘U)">
<Tip :label="`${menu.underline} (${formatShortcut('MOD+U')})`">
<button
class="flex cursor-pointer items-center justify-center rounded border border-border bg-input px-2 py-1 text-muted hover:bg-hover hover:text-surface data-[state=on]:border-accent data-[state=on]:bg-accent data-[state=on]:text-white"
:data-state="ctx.activeFormatting.value.includes('underline') ? 'on' : 'off'"

View file

@ -5,7 +5,7 @@ import { useRoute } from 'vue-router'
import { useHead } from '@unhead/vue'
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
import { useViewportKind } from '@open-pencil/vue'
import { useViewportKind, formatShortcut } from '@open-pencil/vue'
import { useKeyboard } from '@/app/shell/keyboard/use'
import { loadEditorLayout, saveEditorLayout } from '@/app/shell/layout-storage'
import { openFileFromPath, useMenu } from '@/app/shell/menu/use'
@ -194,7 +194,7 @@ onUnmounted(() => {
<button
data-test-id="editor-show-ui"
class="ml-1 flex size-6 cursor-pointer items-center justify-center rounded text-muted transition-colors hover:bg-hover hover:text-surface"
title="Show UI (⌘\)"
:title="`Show UI (${formatShortcut('MOD+\\')})`"
@click="store.state.showUI = true"
>
<icon-lucide-sidebar class="size-3.5" />