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 } 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) && (FONT_SIZE_OPTIONS as readonly number[]).includes(parsed)) return parsed;
}
}
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
49 changes: 46 additions & 3 deletions src/tests/components/SettingsModal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import SettingsModal from '../../components/SettingsModal';

// Mock the store - use inline functions to avoid hoisting issues
const mockSetEditorFontSize = vi.fn();
const mockSetEditorWordWrap = vi.fn();

vi.mock('../../store/store', () => {
return {
default: vi.fn((selector) => selector({
isSettingsOpen: true,
setSettingsOpen: vi.fn(),
showLineNumbers: true,
setShowLineNumbers: vi.fn(),
editorFontSize: 14,
setEditorFontSize: mockSetEditorFontSize,
editorWordWrap: true,
setEditorWordWrap: mockSetEditorWordWrap,
textColor: '#121212',
backgroundColor: '#ffffff',
toggleDarkMode: vi.fn(),
Expand Down Expand Up @@ -76,10 +83,46 @@ 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('calls setEditorWordWrap when word wrap toggle is clicked', () => {
render(<SettingsModal />);

const toggle = screen.getByRole('switch', { name: /toggle word wrap/i });
fireEvent.click(toggle);
expect(mockSetEditorWordWrap).toHaveBeenCalledWith(false, expect.anything());
});

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);
});
});
Loading