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';
import { FONT_SIZE_OPTIONS } from '../constants/editorSettings';


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
4 changes: 4 additions & 0 deletions src/constants/editorSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 18, 20] as const;
export const DEFAULT_FONT_SIZE = 14;
export const MIN_FONT_SIZE = FONT_SIZE_OPTIONS[0];
export const MAX_FONT_SIZE = FONT_SIZE_OPTIONS[FONT_SIZE_OPTIONS.length - 1];
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" as const : "off" as const,
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" as const : "off" as const,
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
41 changes: 41 additions & 0 deletions src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ModelManager } from "@accordproject/concerto-core";
import { TemplateMarkInterpreter } from "@accordproject/template-engine";
import { TemplateMarkTransformer } from "@accordproject/markdown-template";
import { transform } from "@accordproject/markdown-transform";
import { FONT_SIZE_OPTIONS, DEFAULT_FONT_SIZE, MIN_FONT_SIZE, MAX_FONT_SIZE } from "../constants/editorSettings";
import { SAMPLES, Sample } from "../samples";
import * as playground from "../samples/playground";
import { compress, decompress } from "../utils/compression/compression";
Expand Down Expand Up @@ -63,6 +64,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;
keyProtectionLevel: KeyProtectionLevel | null;
Expand Down Expand Up @@ -162,6 +167,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) && parsed >= MIN_FONT_SIZE && parsed <= MAX_FONT_SIZE) return parsed;
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

getInitialFontSize currently accepts any numeric value between MIN_FONT_SIZE and MAX_FONT_SIZE from localStorage. This can load a font size that's not in FONT_SIZE_OPTIONS (e.g., 17), leaving the Settings <Select> with a value that has no matching option and making the UI/state inconsistent. Consider validating the parsed value against FONT_SIZE_OPTIONS (or clamping/mapping to the nearest allowed option) and falling back to DEFAULT_FONT_SIZE otherwise.

Suggested change
const getInitialFontSize = () => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('editorFontSize');
if (saved !== null) {
const parsed = parseInt(saved, 10);
if (!isNaN(parsed) && parsed >= MIN_FONT_SIZE && parsed <= MAX_FONT_SIZE) return parsed;
const getAllowedFontSizes = (): number[] => {
return FONT_SIZE_OPTIONS
.map((option) => {
if (typeof option === "number") {
return option;
}
if (
option &&
typeof option === "object" &&
"value" in option &&
typeof (option as { value: unknown }).value === "number"
) {
return (option as { value: number }).value;
}
return null;
})
.filter((value): value is number => value !== null);
};
const getInitialFontSize = () => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('editorFontSize');
if (saved !== null) {
const parsed = parseInt(saved, 10);
if (!isNaN(parsed)) {
const allowedSizes = getAllowedFontSizes();
if (allowedSizes.includes(parsed)) {
return parsed;
}
}

Copilot uses AI. Check for mistakes.
}
}
return 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 @@ -197,6 +223,8 @@ const useAppStore = create<AppState>()(
isTemplateCollapsed: false,
isDataCollapsed: false,
showLineNumbers: getInitialLineNumbers(),
editorFontSize: getInitialFontSize(),
editorWordWrap: getInitialWordWrap(),
isSettingsOpen: false,
keyProtectionLevel: null,
toggleModelCollapse: () => set((state) => ({ isModelCollapsed: !state.isModelCollapsed })),
Expand All @@ -208,6 +236,19 @@ const useAppStore = create<AppState>()(
}
set({ showLineNumbers: value });
},
setEditorFontSize: (value: number) => {
if (!FONT_SIZE_OPTIONS.includes(value as typeof FONT_SIZE_OPTIONS[number])) return;
if (typeof window !== 'undefined') {
localStorage.setItem('editorFontSize', String(value));
}
set({ editorFontSize: value });
},
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);
});
});

55 changes: 55 additions & 0 deletions src/tests/store/editorFontSize.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 set to 14 after reset', () => {
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 valid 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);
});

it('should reject font sizes outside the allowed options', () => {
const store = useAppStore.getState();

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

store.setEditorFontSize(100);
expect(useAppStore.getState().editorFontSize).toBe(14);
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

This test implies invalid font sizes are "rejected" by resetting to 14, but the store implementation currently just returns early and leaves the previous value unchanged. To make the assertion match the behavior and be more robust, set editorFontSize to a non-default valid value first (e.g., 18), then call setEditorFontSize with an invalid value and assert it remains 18.

Suggested change
store.setEditorFontSize(5);
expect(useAppStore.getState().editorFontSize).toBe(14);
store.setEditorFontSize(100);
expect(useAppStore.getState().editorFontSize).toBe(14);
// Start from a non-default valid size
store.setEditorFontSize(18);
expect(useAppStore.getState().editorFontSize).toBe(18);
// Invalid sizes should not change the current font size
store.setEditorFontSize(5);
expect(useAppStore.getState().editorFontSize).toBe(18);
store.setEditorFontSize(100);
expect(useAppStore.getState().editorFontSize).toBe(18);

Copilot uses AI. Check for mistakes.
});
});
Loading
Loading