* feat(settings): add editor font size setting for chat input and code editor Adds an 'Editor font size' control in Settings > Appearance that sets an absolute px font size for the chat input textarea and the in-app CodeMirror editor. Mirrors the existing terminalFontSize lifecycle. - New store field editorFontSize (default 13, clamp 9-32, step 1) in useUIStore with narrow selectors at each consumer. - Persistence wired through appearanceAutoSave, desktop + runtime API types, and persistence.ts read/normalize. - Settings UI row (NumberInput) with reset to 13, VisibleSetting union entry, OpenChamberPage registration, and search index entry appearance.editor-font-size. - Applied as a post-zoom absolute override on the chat input textarea and on the CodeMirror theme's content rule, leaving gutter/line-number chrome at its existing hardcoded sizes (matches terminal scope). - All 10 locales translated (en, es, fr, ja, ko, pl, pt-BR, uk, zh-CN, zh-TW); no English placeholders in non-English dictionaries. Refs #1325 * fix(codemirror): use unitless lineHeight so it scales with editor font size The & rule in the CodeMirror theme set lineHeight to 1.5rem (~24px), which does not scale when editorFontSize is increased (e.g., 28-32px). This causes overlapping lines at larger font sizes. Change to unitless 1.5, which scales proportionally with whatever fontSize resolves to (dynamic prop or --text-code fallback). Matches browser best practice for proportional leading. Review comment: https://github.com/openchamber/openchamber/pull/2065 --------- Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
bashrusakh
Bohdan Triapitsyn
parent
bfaf62e222
commit
e0229917f8
@@ -1121,6 +1121,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const inputBarOffset = useUIStore((state) => state.inputBarOffset);
|
||||
const persistChatDraft = useUIStore((state) => state.persistChatDraft);
|
||||
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
|
||||
const editorFontSize = useUIStore((state) => state.editorFontSize);
|
||||
const isExpandedInput = useUIStore((state) => state.isExpandedInput);
|
||||
const setExpandedInput = useUIStore((state) => state.setExpandedInput);
|
||||
const setTimelineDialogOpen = useUIStore((state) => state.setTimelineDialogOpen);
|
||||
@@ -5256,6 +5257,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
maxHeight: !isComposerExpanded && textareaSize ? `${textareaSize.maxHeight}px` : undefined,
|
||||
borderTopLeftRadius: chatInputRadius,
|
||||
borderTopRightRadius: chatInputRadius,
|
||||
fontSize: `${editorFontSize}px`,
|
||||
}}
|
||||
rows={1}
|
||||
/>
|
||||
|
||||
@@ -133,6 +133,7 @@ const VisualSectionContent: React.FC = () => {
|
||||
...(!isVSCode ? ['weekStart' as const] : []),
|
||||
'fontSize',
|
||||
'terminalFontSize',
|
||||
'editorFontSize',
|
||||
'fileEditorKeymap',
|
||||
'spacing',
|
||||
'inputBarOffset',
|
||||
|
||||
@@ -245,7 +245,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
|
||||
return mode === 'markdown' ? 'markdown' : 'plain';
|
||||
};
|
||||
|
||||
type VisibleSetting = 'sessionAssist' | 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
|
||||
type VisibleSetting = 'sessionAssist' | 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
|
||||
|
||||
interface OpenChamberVisualSettingsProps {
|
||||
/** Which settings to show. If undefined, shows all. */
|
||||
@@ -289,6 +289,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const setFontSize = useUIStore(state => state.setFontSize);
|
||||
const terminalFontSize = useUIStore(state => state.terminalFontSize);
|
||||
const setTerminalFontSize = useUIStore(state => state.setTerminalFontSize);
|
||||
const editorFontSize = useUIStore(state => state.editorFontSize);
|
||||
const setEditorFontSize = useUIStore(state => state.setEditorFontSize);
|
||||
const uiFont = useUIStore(state => state.uiFont);
|
||||
const setUiFont = useUIStore(state => state.setUiFont);
|
||||
const monoFont = useUIStore(state => state.monoFont);
|
||||
@@ -553,7 +555,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const hasAppearanceSettings = isVSCode
|
||||
? hasLocalizationSettings
|
||||
: (shouldShow('theme') || showMobileLayoutSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart'));
|
||||
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset');
|
||||
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('editorFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset');
|
||||
const hasNavigationSettings = (shouldShow('terminalQuickKeys') && !isMobile) || shouldShow('fileEditorKeymap') || shouldShow('expandedEditorToolbar');
|
||||
const hasBehaviorSettings = shouldShow('mermaidRendering')
|
||||
|| shouldShow('userMessageRendering')
|
||||
@@ -1253,6 +1255,35 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShow('editorFontSize') && (
|
||||
<div data-settings-item="appearance.editor-font-size" className={cn("py-1", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "w-56 shrink-0")}>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.editorFontSize')}</span>
|
||||
</div>
|
||||
<div className={cn("flex items-center gap-2", isMobile ? "w-full" : "w-fit")}>
|
||||
<NumberInput
|
||||
value={editorFontSize}
|
||||
onValueChange={setEditorFontSize}
|
||||
min={9}
|
||||
max={32}
|
||||
step={1}
|
||||
className="w-16"
|
||||
/>
|
||||
<Button size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setEditorFontSize(13)}
|
||||
disabled={editorFontSize === 13}
|
||||
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
|
||||
aria-label={t('settings.openchamber.visual.actions.resetEditorFontSizeAria')}
|
||||
title={t('settings.common.actions.reset')}
|
||||
>
|
||||
<Icon name="restart" className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShow('spacing') && (
|
||||
<div data-settings-item="appearance.spacing-density" className={cn("py-1", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "w-56 shrink-0")}>
|
||||
|
||||
@@ -39,6 +39,7 @@ import { shikiHighlightExtension } from '@/lib/codemirror/shikiHighlight';
|
||||
import { getResolvedShikiTheme } from '@/lib/shiki/appThemeRegistry';
|
||||
import { getLanguageFromExtension } from '@/lib/toolHelpers';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
@@ -244,22 +245,24 @@ const SkillsInstalledPage: React.FC = () => {
|
||||
loadSkillDetails();
|
||||
}, [selectedSkill, isNewSkill, selectedSkillName, skills, skillDraft, getSkillDetail]);
|
||||
|
||||
const editorFontSize = useUIStore((state) => state.editorFontSize);
|
||||
|
||||
const skillEditorExtensions = React.useMemo<Extension[]>(() => {
|
||||
const extensions: Extension[] = [createFlexokiCodeMirrorTheme(currentTheme)];
|
||||
const extensions: Extension[] = [createFlexokiCodeMirrorTheme(currentTheme, { fontSize: editorFontSize })];
|
||||
const markdownExtension = languageByExtension(SKILL_DOCUMENT_PATH);
|
||||
if (markdownExtension) {
|
||||
extensions.push(markdownExtension);
|
||||
}
|
||||
extensions.push(EditorView.lineWrapping);
|
||||
return extensions;
|
||||
}, [currentTheme]);
|
||||
}, [currentTheme, editorFontSize]);
|
||||
|
||||
const supportingFileEditorExtensions = React.useMemo<Extension[]>(() => {
|
||||
const filePath = newFileName.trim() || 'supporting-file.md';
|
||||
// Shiki token colors for code supporting files; markdown stays on lezer.
|
||||
const shikiLanguage = getLanguageFromExtension(filePath);
|
||||
const useShiki = Boolean(shikiLanguage) && shikiLanguage !== 'markdown';
|
||||
const extensions: Extension[] = [createFlexokiCodeMirrorTheme(currentTheme, useShiki ? { syntaxColors: false } : undefined)];
|
||||
const extensions: Extension[] = [createFlexokiCodeMirrorTheme(currentTheme, useShiki ? { syntaxColors: false, fontSize: editorFontSize } : { fontSize: editorFontSize })];
|
||||
const languageExtension = languageByExtension(filePath);
|
||||
if (languageExtension) {
|
||||
extensions.push(languageExtension);
|
||||
@@ -273,7 +276,7 @@ const SkillsInstalledPage: React.FC = () => {
|
||||
}
|
||||
extensions.push(EditorView.lineWrapping);
|
||||
return extensions;
|
||||
}, [currentTheme, newFileName]);
|
||||
}, [currentTheme, newFileName, editorFontSize]);
|
||||
|
||||
const handleDescriptionChange = React.useCallback((nextDescription: string) => {
|
||||
setDescription(nextDescription);
|
||||
|
||||
@@ -2885,17 +2885,19 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [canEdit, isMobile, shortcutOverrides, textViewMode]);
|
||||
|
||||
const editorFontSize = useUIStore((state) => state.editorFontSize);
|
||||
|
||||
const editorExtensions = React.useMemo(() => {
|
||||
if (!selectedFile?.path) {
|
||||
return [createFlexokiCodeMirrorTheme(currentTheme)];
|
||||
return [createFlexokiCodeMirrorTheme(currentTheme, { fontSize: editorFontSize })];
|
||||
}
|
||||
|
||||
// Shiki token colors (worker-backed) match the Shiki file view exactly.
|
||||
// Same language resolver as the view, so both agree on the language. When
|
||||
// Shiki is the color source, drop the lezer token colors to avoid a
|
||||
// competing highlighter (keep the lezer language for indentation/folding).
|
||||
// competing highlighter (Keep the lezer language for indentation/folding).
|
||||
const shikiLanguage = getLanguageFromExtension(selectedFile.path);
|
||||
const extensions = [createFlexokiCodeMirrorTheme(currentTheme, shikiLanguage ? { syntaxColors: false } : undefined)];
|
||||
const extensions = [createFlexokiCodeMirrorTheme(currentTheme, shikiLanguage ? { syntaxColors: false, fontSize: editorFontSize } : { fontSize: editorFontSize })];
|
||||
const language = staticLanguageExtension ?? dynamicLanguageExtension;
|
||||
if (language) {
|
||||
extensions.push(language);
|
||||
@@ -2925,7 +2927,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
}));
|
||||
}
|
||||
return extensions;
|
||||
}, [currentTheme, selectedFile?.path, staticLanguageExtension, dynamicLanguageExtension, wrapLines, isMobile, nudgeEditorSelectionAboveKeyboard]);
|
||||
}, [currentTheme, selectedFile?.path, staticLanguageExtension, dynamicLanguageExtension, wrapLines, isMobile, nudgeEditorSelectionAboveKeyboard, editorFontSize]);
|
||||
|
||||
const pierreTheme = React.useMemo(
|
||||
() => ({ light: lightTheme.metadata.id, dark: darkTheme.metadata.id }),
|
||||
|
||||
@@ -345,12 +345,14 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null }) => {
|
||||
};
|
||||
}, [cancel, commentText, editingDraftId, isMobile, lineSelection]);
|
||||
|
||||
const editorFontSize = useUIStore((state) => state.editorFontSize);
|
||||
|
||||
const editorExtensions = React.useMemo(() => {
|
||||
// Shiki token colors only for code files; markdown keeps the lezer
|
||||
// highlighter (markdown-aware bold headings etc., and no Shiki view to match).
|
||||
const shikiLanguage = resolvedPath ? getLanguageFromExtension(resolvedPath) : null;
|
||||
const useShiki = Boolean(shikiLanguage) && shikiLanguage !== 'markdown';
|
||||
const extensions = [createFlexokiCodeMirrorTheme(currentTheme, useShiki ? { syntaxColors: false } : undefined)];
|
||||
const extensions = [createFlexokiCodeMirrorTheme(currentTheme, useShiki ? { syntaxColors: false, fontSize: editorFontSize } : { fontSize: editorFontSize })];
|
||||
const language = languageByExtension(resolvedPath || 'plan.md');
|
||||
if (language) {
|
||||
extensions.push(language);
|
||||
@@ -364,7 +366,7 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null }) => {
|
||||
}
|
||||
extensions.push(EditorView.lineWrapping);
|
||||
return extensions;
|
||||
}, [currentTheme, resolvedPath]);
|
||||
}, [currentTheme, resolvedPath, editorFontSize]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Saved project plans opened via context panel should work even when session plan mode is off.
|
||||
|
||||
Reference in New Issue
Block a user