Skip to content
Open
59 changes: 58 additions & 1 deletion src/components/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import React from 'react';
import { Modal, Switch } from 'antd';
import { Modal, Select, Switch } from 'antd';
import DarkModeToggle from 'react-dark-mode-toggle';
import useAppStore from '../store/store';

const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 18, 20];
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FONT_SIZE_OPTIONS constant is defined in SettingsModal.tsx but not accessible from the store file. For better maintainability and potential validation, consider either: 1) defining FONT_SIZE_OPTIONS in a shared constants file, or 2) exporting it from SettingsModal.tsx so it can be imported in store.ts if validation is added later. This would create a single source of truth for the valid font size values.

Suggested change
const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 18, 20];
export const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 18, 20];

Copilot uses AI. Check for mistakes.

const SettingsModal: React.FC = () => {
const {
isSettingsOpen,
setSettingsOpen,
showLineNumbers,
setShowLineNumbers,
editorFontSize,
setEditorFontSize,
editorWordWrap,
setEditorWordWrap,
textColor,
backgroundColor,
toggleDarkMode
Expand All @@ -17,6 +23,10 @@ const SettingsModal: React.FC = () => {
setSettingsOpen: state.setSettingsOpen,
showLineNumbers: state.showLineNumbers,
setShowLineNumbers: state.setShowLineNumbers,
editorFontSize: state.editorFontSize,
setEditorFontSize: state.setEditorFontSize,
editorWordWrap: state.editorWordWrap,
setEditorWordWrap: state.setEditorWordWrap,
textColor: state.textColor,
backgroundColor: state.backgroundColor,
toggleDarkMode: state.toggleDarkMode,
Expand Down Expand Up @@ -74,6 +84,53 @@ const SettingsModal: React.FC = () => {
/>
</div>
</div>

<hr className={isDarkMode ? 'border-gray-600' : 'border-gray-200'} />

{/* Font Size Dropdown */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm sm:text-base" style={{ color: textColor }}>
Font Size
</h4>
<p className={`text-xs sm:text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
Adjust font size in code editors
</p>
</div>
<div className="flex-shrink-0">
<Select
value={editorFontSize}
onChange={setEditorFontSize}
style={{ width: 80 }}
aria-label="Editor font size"
options={FONT_SIZE_OPTIONS.map((size) => ({
value: size,
label: `${size}px`,
}))}
/>
</div>
</div>

<hr className={isDarkMode ? 'border-gray-600' : 'border-gray-200'} />

{/* Word Wrap Toggle */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm sm:text-base" style={{ color: textColor }}>
Word Wrap
</h4>
<p className={`text-xs sm:text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
Wrap long lines in code editors
</p>
</div>
<div className="flex-shrink-0">
<Switch
checked={editorWordWrap}
onChange={setEditorWordWrap}
aria-label="Toggle word wrap"
/>
</div>
</div>
</div>
</Modal>
);
Expand Down
9 changes: 6 additions & 3 deletions src/editors/ConcertoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,13 @@ export default function ConcertoEditor({
}: ConcertoEditorProps) {
const { handleSelection, MenuComponent } = useCodeSelection("concerto");
const monacoInstance = useMonaco();
const { error, backgroundColor, aiConfig, showLineNumbers } = useAppStore((state) => ({
const { error, backgroundColor, aiConfig, showLineNumbers, editorFontSize, editorWordWrap } = useAppStore((state) => ({
error: state.error,
backgroundColor: state.backgroundColor,
aiConfig: state.aiConfig,
showLineNumbers: state.showLineNumbers,
editorFontSize: state.editorFontSize,
editorWordWrap: state.editorWordWrap,
}));
const ctoErr = error?.startsWith("c:") ? error : undefined;

Expand All @@ -136,9 +138,10 @@ export default function ConcertoEditor({

const options: monaco.editor.IStandaloneEditorConstructionOptions = useMemo(() => ({
minimap: { enabled: false },
wordWrap: "on",
wordWrap: editorWordWrap ? "on" : "off",
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wordWrap property uses different type assertions across editors. In MarkdownEditor, it uses 'as const' for type narrowing ('on' as const : 'off' as const), while JSONEditor and ConcertoEditor use plain string literals without const assertions. For consistency, all three editors should use the same pattern. The 'as const' approach is more type-safe and explicit.

Suggested change
wordWrap: editorWordWrap ? "on" : "off",
wordWrap: editorWordWrap ? ("on" as const) : ("off" as const),

Copilot uses AI. Check for mistakes.
automaticLayout: true,
scrollBeyondLastLine: false,
fontSize: editorFontSize,
lineNumbers: showLineNumbers ? 'on' : 'off',
autoClosingBrackets: "languageDefined",
autoSurround: "languageDefined",
Expand All @@ -159,7 +162,7 @@ export default function ConcertoEditor({
acceptSuggestionOnCommitCharacter: false,
acceptSuggestionOnEnter: "off",
tabCompletion: "off",
}), [aiConfig?.enableInlineSuggestions, showLineNumbers]);
}), [aiConfig?.enableInlineSuggestions, showLineNumbers, editorFontSize, editorWordWrap]);

const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
editor.onDidChangeCursorSelection(() => {
Expand Down
9 changes: 6 additions & 3 deletions src/editors/JSONEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ export default function JSONEditor({
}) {
const { handleSelection, MenuComponent } = useCodeSelection("json");

const { backgroundColor, aiConfig, showLineNumbers } = useAppStore((state) => ({
const { backgroundColor, aiConfig, showLineNumbers, editorFontSize, editorWordWrap } = useAppStore((state) => ({
backgroundColor: state.backgroundColor,
aiConfig: state.aiConfig,
showLineNumbers: state.showLineNumbers,
editorFontSize: state.editorFontSize,
editorWordWrap: state.editorWordWrap,
}));

const themeName = useMemo(
Expand All @@ -32,9 +34,10 @@ export default function JSONEditor({

const options: monaco.editor.IStandaloneEditorConstructionOptions = useMemo(() => ({
minimap: { enabled: false },
wordWrap: "on",
wordWrap: editorWordWrap ? "on" : "off",
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wordWrap property uses different type assertions across editors. In MarkdownEditor, it uses 'as const' for type narrowing ('on' as const : 'off' as const), while JSONEditor and ConcertoEditor use plain string literals without const assertions. For consistency, all three editors should use the same pattern. The 'as const' approach is more type-safe and explicit.

Suggested change
wordWrap: editorWordWrap ? "on" : "off",
wordWrap: editorWordWrap ? ("on" as const) : ("off" as const),

Copilot uses AI. Check for mistakes.
automaticLayout: true,
scrollBeyondLastLine: false,
fontSize: editorFontSize,
lineNumbers: showLineNumbers ? 'on' : 'off',
inlineSuggest: {
enabled: aiConfig?.enableInlineSuggestions !== false,
Expand All @@ -52,7 +55,7 @@ export default function JSONEditor({
acceptSuggestionOnCommitCharacter: false,
acceptSuggestionOnEnter: "off",
tabCompletion: "off",
}), [aiConfig?.enableInlineSuggestions, showLineNumbers]);
}), [aiConfig?.enableInlineSuggestions, showLineNumbers, editorFontSize, editorWordWrap]);


const handleEditorWillMount = (monacoInstance: typeof monaco) => {
Expand Down
9 changes: 6 additions & 3 deletions src/editors/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ export default function MarkdownEditor({
onEditorReady?: (editor: editor.IStandaloneCodeEditor) => void;
}) {
const { handleSelection, MenuComponent } = useCodeSelection("markdown");
const { backgroundColor, textColor, aiConfig, showLineNumbers } = useAppStore((state) => ({
const { backgroundColor, textColor, aiConfig, showLineNumbers, editorFontSize, editorWordWrap } = useAppStore((state) => ({
backgroundColor: state.backgroundColor,
textColor: state.textColor,
aiConfig: state.aiConfig,
showLineNumbers: state.showLineNumbers,
editorFontSize: state.editorFontSize,
editorWordWrap: state.editorWordWrap,
}));
const monaco = useMonaco();

Expand Down Expand Up @@ -58,9 +60,10 @@ export default function MarkdownEditor({

const editorOptions: editor.IStandaloneEditorConstructionOptions = useMemo(() => ({
minimap: { enabled: false },
wordWrap: "on" as const,
wordWrap: editorWordWrap ? "on" as const : "off" as const,
automaticLayout: true,
scrollBeyondLastLine: false,
fontSize: editorFontSize,
lineNumbers: showLineNumbers ? 'on' : 'off',
inlineSuggest: {
enabled: aiConfig?.enableInlineSuggestions !== false,
Expand All @@ -78,7 +81,7 @@ export default function MarkdownEditor({
acceptSuggestionOnCommitCharacter: false,
acceptSuggestionOnEnter: "off",
tabCompletion: "off",
}), [aiConfig?.enableInlineSuggestions, showLineNumbers]);
}), [aiConfig?.enableInlineSuggestions, showLineNumbers, editorFontSize, editorWordWrap]);

const handleEditorDidMount = (editorInstance: editor.IStandaloneCodeEditor) => {
editorInstance.onDidChangeCursorSelection(() => {
Expand Down
39 changes: 39 additions & 0 deletions src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ interface AppState {
toggleDataCollapse: () => void;
showLineNumbers: boolean;
setShowLineNumbers: (value: boolean) => void;
editorFontSize: number;
setEditorFontSize: (value: number) => void;
editorWordWrap: boolean;
setEditorWordWrap: (value: boolean) => void;
isSettingsOpen: boolean;
setSettingsOpen: (value: boolean) => void;
}
Expand Down Expand Up @@ -160,6 +164,27 @@ const getInitialLineNumbers = () => {
return true; // Default to showing line numbers
};

const getInitialFontSize = () => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('editorFontSize');
if (saved !== null) {
const parsed = parseInt(saved, 10);
if (!isNaN(parsed)) return parsed;
}
}
return 14; // Default font size
};
Comment on lines +170 to +179
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getInitialFontSize function doesn't validate that the parsed font size is within the allowed range (12-20). If localStorage contains an invalid value like 5 or 100, it will be accepted. Consider adding validation to ensure the parsed value is within FONT_SIZE_OPTIONS or at least within a reasonable range before returning it.

Copilot uses AI. Check for mistakes.

const getInitialWordWrap = () => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('editorWordWrap');
if (saved !== null) {
return saved === 'true';
}
}
return true; // Default to word wrap on
};

const useAppStore = create<AppState>()(
immer(
devtools((set, get) => {
Expand Down Expand Up @@ -195,6 +220,8 @@ const useAppStore = create<AppState>()(
isTemplateCollapsed: false,
isDataCollapsed: false,
showLineNumbers: getInitialLineNumbers(),
editorFontSize: getInitialFontSize(),
editorWordWrap: getInitialWordWrap(),
isSettingsOpen: false,
toggleModelCollapse: () => set((state) => ({ isModelCollapsed: !state.isModelCollapsed })),
toggleTemplateCollapse: () => set((state) => ({ isTemplateCollapsed: !state.isTemplateCollapsed })),
Expand All @@ -205,6 +232,18 @@ const useAppStore = create<AppState>()(
}
set({ showLineNumbers: value });
},
setEditorFontSize: (value: number) => {
if (typeof window !== 'undefined') {
localStorage.setItem('editorFontSize', String(value));
}
set({ editorFontSize: value });
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setEditorFontSize function doesn't validate that the value is within the allowed FONT_SIZE_OPTIONS array (12, 13, 14, 15, 16, 18, 20). This could allow invalid font sizes to be set programmatically or through manual localStorage modification. Consider adding validation to ensure only valid font sizes from FONT_SIZE_OPTIONS can be set, or at least validate it's a positive number within a reasonable range.

Suggested change
if (typeof window !== 'undefined') {
localStorage.setItem('editorFontSize', String(value));
}
set({ editorFontSize: value });
// Validate and normalize the incoming font size to prevent invalid values
const safeValue = Math.round(value);
const MIN_FONT_SIZE = 8;
const MAX_FONT_SIZE = 72;
if (!Number.isFinite(safeValue) || safeValue < MIN_FONT_SIZE || safeValue > MAX_FONT_SIZE) {
// Ignore invalid values rather than persisting them
return;
}
if (typeof window !== 'undefined') {
localStorage.setItem('editorFontSize', String(safeValue));
}
set({ editorFontSize: safeValue });

Copilot uses AI. Check for mistakes.
},
setEditorWordWrap: (value: boolean) => {
if (typeof window !== 'undefined') {
localStorage.setItem('editorWordWrap', String(value));
}
set({ editorWordWrap: value });
},
setSettingsOpen: (value: boolean) => set({ isSettingsOpen: value }),
setEditorsVisible: (value) => {
const state = get();
Expand Down
37 changes: 35 additions & 2 deletions src/tests/components/SettingsModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ vi.mock('../../store/store', () => {
setSettingsOpen: vi.fn(),
showLineNumbers: true,
setShowLineNumbers: vi.fn(),
editorFontSize: 14,
setEditorFontSize: vi.fn(),
editorWordWrap: true,
setEditorWordWrap: vi.fn(),
textColor: '#121212',
backgroundColor: '#ffffff',
toggleDarkMode: vi.fn(),
Expand Down Expand Up @@ -76,10 +80,39 @@ describe('SettingsModal', () => {
expect(toggle).toBeChecked();
});

it('renders the Font Size setting', () => {
render(<SettingsModal />);

expect(screen.getByText('Font Size')).toBeInTheDocument();
expect(screen.getByText('Adjust font size in code editors')).toBeInTheDocument();
});

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test for Font Size setting only verifies that the text labels are rendered, but doesn't verify that the Select component itself is rendered or that it displays the correct initial value (14). Consider adding a test similar to the word wrap toggle tests that verifies the Select component is present and shows the correct value from the store.

Suggested change
it('renders the Font Size select with the correct initial value', () => {
render(<SettingsModal />);
const fontSizeSelect = screen.getByRole('combobox', { name: /font size/i });
expect(fontSizeSelect).toBeInTheDocument();
expect(fontSizeSelect).toHaveValue('14');
});

Copilot uses AI. Check for mistakes.
it('renders the Word Wrap setting', () => {
render(<SettingsModal />);

expect(screen.getByText('Word Wrap')).toBeInTheDocument();
expect(screen.getByText('Wrap long lines in code editors')).toBeInTheDocument();
});

it('renders the word wrap toggle switch', () => {
render(<SettingsModal />);

const toggle = screen.getByRole('switch', { name: /toggle word wrap/i });
expect(toggle).toBeInTheDocument();
});

it('word wrap toggle is checked when editorWordWrap is true', () => {
render(<SettingsModal />);

const toggle = screen.getByRole('switch', { name: /toggle word wrap/i });
expect(toggle).toBeChecked();
});

it('renders divider between settings', () => {
render(<SettingsModal />);

const divider = document.querySelector('hr');
expect(divider).toBeInTheDocument();
const dividers = document.querySelectorAll('hr');
expect(dividers.length).toBeGreaterThanOrEqual(1);
});
});

45 changes: 45 additions & 0 deletions src/tests/store/editorFontSize.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, it, expect, beforeEach } from 'vitest';
import useAppStore from '../../store/store';

describe('useAppStore - editorFontSize', () => {
beforeEach(() => {
localStorage.clear();
useAppStore.setState({
editorFontSize: 14,
});
});

it('should have editorFontSize default to 14', () => {
const state = useAppStore.getState();
expect(state.editorFontSize).toBe(14);
});
Comment on lines +5 to +15
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests set editorFontSize in beforeEach, so the “default to 14” assertion doesn’t actually verify the store’s initialization/default logic (or localStorage bootstrapping) and will pass even if the real default changes. Consider restructuring to test initialization separately (e.g., by exporting init helpers, or by re-importing the store after setting localStorage via vi.resetModules()) and keep direct setState only for non-default behavior tests.

Copilot generated this review using guidance from repository custom instructions.

it('should update editorFontSize when setEditorFontSize is called', () => {
const store = useAppStore.getState();

store.setEditorFontSize(18);

expect(useAppStore.getState().editorFontSize).toBe(18);
});

it('should persist editorFontSize to localStorage', () => {
const store = useAppStore.getState();

store.setEditorFontSize(16);

expect(localStorage.getItem('editorFontSize')).toBe('16');
});

it('should set various font sizes correctly', () => {
const store = useAppStore.getState();

store.setEditorFontSize(12);
expect(useAppStore.getState().editorFontSize).toBe(12);

store.setEditorFontSize(20);
expect(useAppStore.getState().editorFontSize).toBe(20);

store.setEditorFontSize(14);
expect(useAppStore.getState().editorFontSize).toBe(14);
});
});
Loading
Loading