diff --git a/.changeset/web-ui-font-family.md b/.changeset/web-ui-font-family.md new file mode 100644 index 0000000000..b0cda6b640 --- /dev/null +++ b/.changeset/web-ui-font-family.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Add font family preferences to Appearance settings (desktop dialog + mobile sheet). The UI/reading font offers Default (Inter), System, Serif, and Custom faces; the code font offers Default (JetBrains Mono), System, and Custom. Custom shows a dropdown of locally installed fonts (detected by probing a curated candidate list, e.g. Maple Mono NF CN) with a manual entry fallback; the default stack is appended as fallback so nothing extra is downloaded. Choices remap the `--font-ui` / `--font-mono` tokens via `` / ``, so every surface picks them up with no component changes; the xterm terminal keeps its fixed literal stack. diff --git a/apps/kimi-web/src/App.vue b/apps/kimi-web/src/App.vue index 5708bcc9bf..2cab827502 100644 --- a/apps/kimi-web/src/App.vue +++ b/apps/kimi-web/src/App.vue @@ -990,6 +990,10 @@ function openPr(url: string): void { :color-scheme="client.colorScheme.value" :accent="client.accent.value" :ui-font-size="client.uiFontSize.value" + :ui-font-family="client.uiFontFamily.value" + :ui-custom-font="client.uiCustomFont.value" + :code-font-family="client.codeFontFamily.value" + :code-custom-font="client.codeCustomFont.value" :auth-ready="client.authReady.value" :account-model="client.defaultModel.value" :notify="client.notifyOnComplete.value" @@ -1006,6 +1010,10 @@ function openPr(url: string): void { @set-color-scheme="client.setColorScheme($event)" @set-accent="client.setAccent($event)" @set-ui-font-size="client.setUiFontSize($event)" + @set-ui-font-family="client.setUiFontFamily($event)" + @set-ui-custom-font="client.setUiCustomFont($event)" + @set-code-font-family="client.setCodeFontFamily($event)" + @set-code-custom-font="client.setCodeCustomFont($event)" @set-notify="client.setNotifyOnComplete($event)" @set-notify-question="client.setNotifyOnQuestion($event)" @set-notify-approval="client.setNotifyOnApproval($event)" @@ -1108,6 +1116,10 @@ function openPr(url: string): void { :swarm-mode="client.swarmMode.value" :color-scheme="client.colorScheme.value" :ui-font-size="client.uiFontSize.value" + :ui-font-family="client.uiFontFamily.value" + :ui-custom-font="client.uiCustomFont.value" + :code-font-family="client.codeFontFamily.value" + :code-custom-font="client.codeCustomFont.value" :auth-ready="client.authReady.value" :conversation-toc="client.conversationToc.value" :server-version="client.serverVersion.value" @@ -1118,6 +1130,10 @@ function openPr(url: string): void { @set-permission="client.setPermission($event)" @set-color-scheme="client.setColorScheme($event)" @set-ui-font-size="client.setUiFontSize($event)" + @set-ui-font-family="client.setUiFontFamily($event)" + @set-ui-custom-font="client.setUiCustomFont($event)" + @set-code-font-family="client.setCodeFontFamily($event)" + @set-code-custom-font="client.setCodeCustomFont($event)" @set-conversation-toc="client.setConversationToc($event)" @login="() => { showMobileSettings = false; openLogin(); }" @logout="client.logout" diff --git a/apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue b/apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue index 6e80a74fe1..096ba98eea 100644 --- a/apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue +++ b/apps/kimi-web/src/components/mobile/MobileSettingsSheet.vue @@ -10,7 +10,7 @@ import { computed, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import type { ConversationStatus, PermissionMode } from '../../types'; import type { AppModel, AppSession, ThinkingLevel } from '../../api/types'; -import type { ColorScheme } from '../../composables/useKimiWebClient'; +import type { CodeFontFamily, ColorScheme, UiFontFamily } from '../../composables/useKimiWebClient'; import { useKimiWebClient } from '../../composables/useKimiWebClient'; import { commitLevel, @@ -22,9 +22,11 @@ import { import BottomSheet from '../dialogs/BottomSheet.vue'; import LanguageSwitcher from '../settings/LanguageSwitcher.vue'; import { formatTokens } from '../../lib/formatTokens'; +import { CODE_FONT_CANDIDATES, UI_FONT_CANDIDATES, installedCandidates } from '../../lib/fontCandidates'; import Button from '../ui/Button.vue'; import Input from '../ui/Input.vue'; import SegmentedControl from '../ui/SegmentedControl.vue'; +import Select from '../ui/Select.vue'; const { t } = useI18n(); @@ -37,6 +39,10 @@ const props = withDefaults( swarmMode?: boolean; colorScheme?: ColorScheme; uiFontSize?: number; + uiFontFamily?: UiFontFamily; + uiCustomFont?: string; + codeFontFamily?: CodeFontFamily; + codeCustomFont?: string; authReady?: boolean; conversationToc?: boolean; /** Server version from GET /api/v1/meta, shown as a read-only row. */ @@ -47,6 +53,10 @@ const props = withDefaults( { colorScheme: 'system', uiFontSize: 14, + uiFontFamily: 'default', + uiCustomFont: '', + codeFontFamily: 'default', + codeCustomFont: '', authReady: false, serverVersion: '', models: () => [], @@ -62,6 +72,10 @@ const emit = defineEmits<{ setPermission: [mode: PermissionMode]; setColorScheme: [colorScheme: ColorScheme]; setUiFontSize: [size: number]; + setUiFontFamily: [font: UiFontFamily]; + setUiCustomFont: [name: string]; + setCodeFontFamily: [font: CodeFontFamily]; + setCodeCustomFont: [name: string]; setConversationToc: [on: boolean]; login: []; logout: []; @@ -71,6 +85,57 @@ function onColorScheme(v: string): void { emit('setColorScheme', v as ColorScheme); } +function onUiFontFamily(v: string): void { + emit('setUiFontFamily', v as UiFontFamily); +} + +function onCodeFontFamily(v: string): void { + emit('setCodeFontFamily', v as CodeFontFamily); +} + +// Custom font dropdowns: the browser cannot enumerate installed fonts, so the +// select probes a curated candidate list (lib/fontCandidates) and offers the +// installed ones; the last option reveals a free-text input for unlisted +// faces. A stored name that is not detected is kept as a select option so the +// current value always renders. +const MANUAL_FONT_VALUE = '__manual__'; + +const uiFontManual = ref(false); +const codeFontManual = ref(false); + +const uiFontOptions = computed(() => { + const installed = installedCandidates(UI_FONT_CANDIDATES); + const current = props.uiCustomFont.trim(); + return current !== '' && !installed.includes(current) ? [current, ...installed] : installed; +}); + +const codeFontOptions = computed(() => { + const installed = installedCandidates(CODE_FONT_CANDIDATES); + const current = props.codeCustomFont.trim(); + return current !== '' && !installed.includes(current) ? [current, ...installed] : installed; +}); + +const uiFontSelectValue = computed(() => (uiFontManual.value ? MANUAL_FONT_VALUE : props.uiCustomFont)); +const codeFontSelectValue = computed(() => (codeFontManual.value ? MANUAL_FONT_VALUE : props.codeCustomFont)); + +function onUiFontSelect(value: string): void { + if (value === MANUAL_FONT_VALUE) { + uiFontManual.value = true; + return; + } + uiFontManual.value = false; + emit('setUiCustomFont', value); +} + +function onCodeFontSelect(value: string): void { + if (value === MANUAL_FONT_VALUE) { + codeFontManual.value = true; + return; + } + codeFontManual.value = false; + emit('setCodeCustomFont', value); +} + const PERM_MODES: PermissionMode[] = ['manual', 'auto', 'yolo']; // Identity is the model id — display/model names can collide across providers. @@ -364,6 +429,103 @@ watch( +
+ + {{ t('theme.fontLabel') }} + + +
+ +
+ + {{ t('theme.fontCustomLabel') }} + + + + +
+ +
+ + {{ t('theme.fontCustomManualLabel') }} + + + + +
+ +
+ + {{ t('theme.codeFontLabel') }} + + +
+ +
+ + {{ t('theme.codeFontCustomLabel') }} + + + + +
+ +
+ + {{ t('theme.fontCustomManualLabel') }} + + + + +
+