diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 88e1c1df30..d5dd4dd75b 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -239,6 +239,7 @@ import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, normalizeCustomF import { buildFontFamilyOptions, displayFontFamily, isPresetFontFamily, loadSystemFontNames } from "@/lib/app/fontFamilyOptions"; import { buildAppSupportInfoRows, formatAppSupportInfoForClipboard, type AppSupportInfoLabels } from "@/lib/app/supportInfo"; import { useUiFontFamilyPreview } from "@/composables/useUiFontFamilyPreview"; +import { useMeasuredWidth } from "@/composables/useMeasuredWidth"; import { DateTimePatterns, normalizeSupportedDateTimePattern } from "@/lib/dataGrid/columnFormatter"; import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE } from "@/lib/dataGrid/paginationPageSize"; import { MAX_QUERY_RESULT_MAX_ROWS } from "@/lib/dataGrid/queryResultRowLimit"; @@ -2187,7 +2188,21 @@ function formatShortcutPill(shortcut: string): string { } const shortcutPressShortcutLabel = computed(() => t("settings.shortcutPressShortcut")); -const shortcutPressShortcutInputWidth = computed(() => `${shortcutPressShortcutLabel.value.length + 2}em`); +// #9144: `label.length + 2em` under-sizes CJK placeholders wherever the +// environment's fallback font advances wider than 1em (user report: 15px/char +// at a 13px font → the last glyph clipped mid-stroke). Measure a hidden mirror +// span carrying the same box + typography classes as the capture inputs +// instead; the char-count formula only survives as the pre-measurement +// fallback. The +2px cushion absorbs sub-pixel rounding differences between +// the mirror span and the real input. +const shortcutPressShortcutInputWidth = ref(`${shortcutPressShortcutLabel.value.length + 2}em`); +const shortcutPlaceholderMirrorRef = ref(null); +const shortcutPlaceholderMirrorWidth = useMeasuredWidth(shortcutPlaceholderMirrorRef, 0, [shortcutPressShortcutLabel]); +watch(shortcutPlaceholderMirrorWidth, (width) => { + if (width > 0) { + shortcutPressShortcutInputWidth.value = `${Math.ceil(width) + 2}px`; + } +}); function focusShortcutInput(actionId: ShortcutActionId) { editingShortcutId.value = actionId; @@ -9724,6 +9739,13 @@ LIMIT 100; + + + diff --git a/apps/desktop/src/components/editor/__tests__/EditorSettingsDialog.shortcutCaptureInputWidth.spec.ts b/apps/desktop/src/components/editor/__tests__/EditorSettingsDialog.shortcutCaptureInputWidth.spec.ts new file mode 100644 index 0000000000..5e012ffb1a --- /dev/null +++ b/apps/desktop/src/components/editor/__tests__/EditorSettingsDialog.shortcutCaptureInputWidth.spec.ts @@ -0,0 +1,74 @@ +// @vitest-environment happy-dom +// Source-assertion regression for #9144: the shortcut-capture input width must +// come from measuring a hidden mirror span (same box + typography as the real +// inputs), not from `label.length + 2em` char-count arithmetic — CJK fallback +// fonts can advance wider than 1em and clip the last placeholder glyph. The +// dialog cannot be mounted (a 10k-line SFC whose imports pull the whole +// editor + stores), so this follows the same `?raw` pattern as +// EditorSettingsDialog.mcpQueryTimeout.integration.spec.ts. +import { describe, expect, it } from "vitest"; +import dialogSource from "../EditorSettingsDialog.vue?raw"; + +function templateOnly(): string { + // The root " — this SFC is full + // of nested slot