${escapeHtml(code)}`
@@ -56,9 +81,10 @@ export function useShikiHighlighter() {
const normalized = (lang || 'plaintext').toLowerCase()
const hl = await getHighlighter()
await ensureLang(hl, normalized)
+ const themes = await ensurePairedThemes(hl)
html.value = hl.codeToHtml(code, {
lang: loadedLangs.has(normalized) ? normalized : 'plaintext',
- themes: { light: 'github-light', dark: 'github-dark' },
+ themes,
})
} catch {
html.value = `${escapeHtml(code)}`
@@ -73,8 +99,8 @@ export function useShikiHighlighter() {
const lang = getLanguageByFilename(filename)
const hl = await getHighlighter()
await ensureLang(hl, lang)
+ const themes = await ensurePairedThemes(hl)
const effectiveLang = loadedLangs.has(lang) ? lang : 'plaintext'
- const themes = { light: 'github-light', dark: 'github-dark' }
const oldHtml = oldText
? hl.codeToHtml(oldText, { lang: effectiveLang, themes })
@@ -95,19 +121,44 @@ export function useShikiHighlighter() {
async function highlightLanguage(code: string, lang: string, options: {
theme?: BundledTheme
+ // Explicit dual-theme override. Pass when the call site needs to dodge the
+ // `.dark .shiki span` !important rule that ships in the design system: set
+ // both halves to the same theme and shiki emits `--shiki-dark` equal to the
+ // light value, so the override resolves back to the picked colors.
+ themes?: { light: BundledTheme, dark: BundledTheme }
transparentPre?: boolean
} = {}) {
loading.value = true
try {
const hl = await getHighlighter()
await ensureLang(hl, lang)
- html.value = hl.codeToHtml(code, {
- lang: loadedLangs.has(lang) ? lang : 'plaintext',
- ...(options.theme
- ? { theme: options.theme }
- : { themes: { light: 'github-light', dark: 'github-dark' } }),
- transformers: options.transparentPre ? [transparentPreTransformer] : undefined,
- })
+ const effectiveLang = loadedLangs.has(lang) ? lang : 'plaintext'
+ const transformers = options.transparentPre ? [transparentPreTransformer] : undefined
+ if (options.theme) {
+ await ensureTheme(hl, options.theme)
+ html.value = hl.codeToHtml(code, {
+ lang: effectiveLang,
+ theme: options.theme,
+ transformers,
+ })
+ } else if (options.themes) {
+ await Promise.all([
+ ensureTheme(hl, options.themes.light),
+ ensureTheme(hl, options.themes.dark),
+ ])
+ html.value = hl.codeToHtml(code, {
+ lang: effectiveLang,
+ themes: options.themes,
+ transformers,
+ })
+ } else {
+ const themes = await ensurePairedThemes(hl)
+ html.value = hl.codeToHtml(code, {
+ lang: effectiveLang,
+ themes,
+ transformers,
+ })
+ }
} catch {
html.value = `${escapeHtml(code)}`
} finally {
diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json
index c3168b52f..05d5eb861 100644
--- a/apps/web/src/i18n/locales/en.json
+++ b/apps/web/src/i18n/locales/en.json
@@ -210,6 +210,13 @@
"codeFontFamilyDescription": "Overrides the default code font",
"codeFontSize": "Code Font Size",
"codeFontSizeDescription": "Applies to code editors, code blocks, and the terminal",
+ "syntaxHighlighting": "Syntax Highlighting",
+ "shikiThemeLight": "Light theme",
+ "shikiThemeLightDescription": "Applied to code blocks and editors in light mode",
+ "shikiThemeDark": "Dark theme",
+ "shikiThemeDarkDescription": "Applied to code blocks and editors in dark mode",
+ "shikiThemeSearch": "Search themes…",
+ "shikiThemeEmpty": "No themes found.",
"colorSchemes": {
"memoh": "Memoh",
"ocean": "Ocean",
@@ -223,7 +230,12 @@
"forest": "Subtle green tones",
"rose": "Warm accents and soft contrast",
"amber": "Golden highlights with clear status indicators"
- }
+ },
+ "codeHighlight": "Code highlight",
+ "codeHighlightDescription": "Themes applied to code blocks and editors.",
+ "diagrams": "Diagrams",
+ "mermaidTheme": "Mermaid Theme",
+ "mermaidThemeDescription": "Applies to rendered Mermaid diagrams."
},
"keyboard": {
"title": "Keyboard Shortcuts",
diff --git a/apps/web/src/i18n/locales/ja.json b/apps/web/src/i18n/locales/ja.json
index 66a402331..4315cdad2 100644
--- a/apps/web/src/i18n/locales/ja.json
+++ b/apps/web/src/i18n/locales/ja.json
@@ -204,6 +204,13 @@
"codeFontFamilyDescription": "標準のコードフォントを上書きします",
"codeFontSize": "コードフォントサイズ",
"codeFontSizeDescription": "コードエディタ、コードブロック、ターミナルに適用されます",
+ "syntaxHighlighting": "Syntax Highlight",
+ "shikiThemeLight": "Light テーマ",
+ "shikiThemeLightDescription": "Light モード時のコードブロックとエディタに適用されます",
+ "shikiThemeDark": "Dark テーマ",
+ "shikiThemeDarkDescription": "Dark モード時のコードブロックとエディタに適用されます",
+ "shikiThemeSearch": "テーマを検索…",
+ "shikiThemeEmpty": "該当するテーマがありません。",
"colorSchemes": {
"memoh": "Memoh",
"ocean": "海",
@@ -217,7 +224,12 @@
"forest": "控えめなグリーントーン",
"rose": "温かみのあるアクセントと柔らかなコントラスト",
"amber": "明確なステータスインジケーターを備えた金色のハイライト"
- }
+ },
+ "codeHighlight": "コードハイライト",
+ "codeHighlightDescription": "コードブロックとエディタに適用される配色。",
+ "diagrams": "ダイアグラム",
+ "mermaidTheme": "Mermaid テーマ",
+ "mermaidThemeDescription": "レンダリング済みの Mermaid 図表に適用されます。"
},
"keyboard": {
"title": "キーボードショートカット",
diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json
index 193581281..232c536e5 100644
--- a/apps/web/src/i18n/locales/zh.json
+++ b/apps/web/src/i18n/locales/zh.json
@@ -206,6 +206,13 @@
"codeFontFamilyDescription": "替换默认代码字体",
"codeFontSize": "代码字号",
"codeFontSizeDescription": "应用于代码编辑器、代码块与终端",
+ "syntaxHighlighting": "语法高亮",
+ "shikiThemeLight": "浅色主题",
+ "shikiThemeLightDescription": "浅色模式下用于代码块与编辑器",
+ "shikiThemeDark": "深色主题",
+ "shikiThemeDarkDescription": "深色模式下用于代码块与编辑器",
+ "shikiThemeSearch": "搜索主题…",
+ "shikiThemeEmpty": "未找到主题。",
"colorSchemes": {
"memoh": "Memoh",
"ocean": "海洋",
@@ -219,7 +226,12 @@
"forest": "沉静的绿色调方案",
"rose": "温暖的玫瑰色,柔和的视觉对比",
"amber": "明亮的琥珀色,清晰的状态指示"
- }
+ },
+ "codeHighlight": "代码高亮",
+ "codeHighlightDescription": "应用于代码块与编辑器的配色方案。",
+ "diagrams": "图表",
+ "mermaidTheme": "Mermaid 主题",
+ "mermaidThemeDescription": "应用于已渲染的 Mermaid 图表。"
},
"keyboard": {
"title": "快捷键",
diff --git a/apps/web/src/pages/about/index.vue b/apps/web/src/pages/about/index.vue
index b2bff758a..aa605d044 100644
--- a/apps/web/src/pages/about/index.vue
+++ b/apps/web/src/pages/about/index.vue
@@ -150,6 +150,7 @@
:typewriter="false"
:fade="false"
:show-tooltips="false"
+ :theme="codeBlockTheme"
custom-id="release-notes"
/>
@@ -192,6 +193,7 @@ import { useI18n } from 'vue-i18n'
import SettingsRow from '@/components/settings/row.vue'
import SettingsSection from '@/components/settings/section.vue'
import { useCapabilitiesStore } from '@/store/capabilities'
+import { useSettingsStore } from '@/store/settings'
import { useUpdateStore } from '@/store/update'
const GITHUB_REPO = 'memohai/memoh'
@@ -209,7 +211,12 @@ const { serverVersion, commitHash } = storeToRefs(capabilitiesStore)
const normalizedServerVersion = computed(() => (serverVersion.value ?? '').replace(/^v/i, ''))
const update = useUpdateStore()
+const settingsStore = useSettingsStore()
const isDark = useDark()
+const codeBlockTheme = computed(() => ({
+ light: settingsStore.shikiThemeLight,
+ dark: settingsStore.shikiThemeDark,
+}))
const links: ResourceLink[] = [
{ icon: Github, labelKey: 'about.github', href: `https://github.com/${GITHUB_REPO}` },
diff --git a/apps/web/src/pages/appearance/index.vue b/apps/web/src/pages/appearance/index.vue
index 4f08c6404..31b3d30a1 100644
--- a/apps/web/src/pages/appearance/index.vue
+++ b/apps/web/src/pages/appearance/index.vue
@@ -151,40 +151,133 @@
/>
+ + {{ t('settings.appearance.codeHighlightDescription') }} +
+- {{ t('settings.appearance.codeFontFamilyDescription') }} + {{ t('settings.appearance.mermaidThemeDescription') }}
${codeFontPreviewCode}`)
+const codeFontPreviewFallback = `${codeFontPreviewCode}`
+const codeFontPreviewLightHtml = computed(() => codeFontPreviewLight.html.value || codeFontPreviewFallback)
+const codeFontPreviewDarkHtml = computed(() => codeFontPreviewDark.html.value || codeFontPreviewFallback)
const codeFontPreviewStyle = computed(() => ({
'--typography-code-preview-font-family': cssFontFamilyDeclaration(codeFontFamilyDraft.value, DEFAULT_CODE_FONT_FAMILY),
'--typography-code-preview-font-size': `${normalizeCodeFontSizePx(codeFontSizeDraft.value)}px`,
}))
function renderCodeFontPreview() {
- void codeFontPreview.highlightLanguage(codeFontPreviewCode, 'typescript', {
- theme: isDark.value ? 'github-dark' : 'github-light',
- transparentPre: true,
+ // Both halves of each preview use the SAME picked theme. The design system's
+ // `.dark .shiki span` !important rule forces every span color to var(--shiki-dark)
+ // in dark interface mode; pinning both halves means that variable already equals
+ // the chosen theme's colors, so the cascade override is a visual no-op and each
+ // preview stays true to its picked theme regardless of interface mode.
+ const light = shikiThemeLight.value as BundledTheme
+ const dark = shikiThemeDark.value as BundledTheme
+ void codeFontPreviewLight.highlightLanguage(codeFontPreviewCode, 'typescript', {
+ themes: { light, dark: light },
+ })
+ void codeFontPreviewDark.highlightLanguage(codeFontPreviewCode, 'typescript', {
+ themes: { light: dark, dark },
})
}
@@ -272,7 +442,7 @@ watch(uiFontSizePx, (value) => { uiFontSizeDraft.value = String(value) })
watch(codeFontSizePx, (value) => { codeFontSizeDraft.value = String(value) })
watch(uiFontFamily, (value) => { uiFontFamilyDraft.value = value })
watch(codeFontFamily, (value) => { codeFontFamilyDraft.value = value })
-watch(isDark, () => { renderCodeFontPreview() })
+watch([shikiThemeLight, shikiThemeDark], () => { renderCodeFontPreview() })
function updateUiFontSizeDraft(value: string | number) { uiFontSizeDraft.value = String(value) }
function updateCodeFontSizeDraft(value: string | number) { codeFontSizeDraft.value = String(value) }
@@ -309,3 +479,34 @@ function commitCodeFontFamilyDraft() {
codeFontFamilyDraft.value = codeFontFamily.value
}
+
+
diff --git a/apps/web/src/pages/home/components/message-item.vue b/apps/web/src/pages/home/components/message-item.vue
index e0f487489..7c5c11c6b 100644
--- a/apps/web/src/pages/home/components/message-item.vue
+++ b/apps/web/src/pages/home/components/message-item.vue
@@ -123,6 +123,7 @@
:fade="message.streaming"
:show-tooltips="false"
:mermaid-props="{ showTooltips: false }"
+ :theme="codeBlockTheme"
custom-id="chat-msg"
/>