Add distributed settings and workspace import/export (M5)

This commit is contained in:
2026-06-07 15:51:00 +03:00
parent 80bedd2a8d
commit 548aa199d9
38 changed files with 3150 additions and 101 deletions
+120 -7
View File
@@ -30,7 +30,16 @@ import { hasInlineData } from '../stores/ExtractStore';
import { notify } from '../stores/NotificationStore';
import { usePreviewStore } from '../stores/PreviewStore';
import { selectActiveSnippet, selectShownText, useSnippetStore } from '../stores/SnippetStore';
import { useUserSettingsStore } from '../stores/UserSettingsStore';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
import {
NumberControl,
RangeControl,
ResetButton,
SettingFooter,
SettingRow,
SettingsPopover,
} from './SettingsPopover';
import type { EditorView } from '../stores/SnippetStore';
import styles from './SpecEditor.module.css';
@@ -40,6 +49,87 @@ const VIEW_OPTIONS: ReadonlyArray<SegmentedOption<EditorView>> = [
{ value: 'published', label: 'Published' },
];
/** Editor syntax theme: Auto follows the app theme; overrides force one (spec §07). */
const EDITOR_THEME_OPTIONS: ReadonlyArray<SegmentedOption<string>> = [
{ value: 'auto', label: 'Auto' },
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
];
const ONOFF_OPTIONS: ReadonlyArray<SegmentedOption<'on' | 'off'>> = [
{ value: 'on', label: 'On' },
{ value: 'off', label: 'Off' },
];
/**
* Editor settings cluster (spec §07 → Editor), disclosed from the editor toolbar
* and applied live. The id matches the Cmd/Ctrl+, shortcut target (App.tsx).
*/
function EditorSettings() {
const editor = useUserSettingsStore((s) => s.saved.editor);
const setEditor = useUserSettingsStore((s) => s.setEditor);
const resetEditor = useUserSettingsStore((s) => s.resetEditor);
return (
<SettingsPopover id="editor-settings" label="Editor settings" title="Editor">
<SettingRow label="Font size" htmlFor="set-fontsize">
<RangeControl
id="set-fontsize"
min={10}
max={18}
value={editor.fontSize}
suffix="px"
onChange={(fontSize) => setEditor({ fontSize })}
/>
</SettingRow>
<SettingRow label="Theme">
<SegmentedControl
label="Editor theme"
options={EDITOR_THEME_OPTIONS}
value={editor.theme}
onChange={(theme) => setEditor({ theme })}
/>
</SettingRow>
<SettingRow label="Minimap">
<SegmentedControl
label="Minimap"
options={ONOFF_OPTIONS}
value={editor.minimap ? 'on' : 'off'}
onChange={(v) => setEditor({ minimap: v === 'on' })}
/>
</SettingRow>
<SettingRow label="Word wrap">
<SegmentedControl
label="Word wrap"
options={ONOFF_OPTIONS}
value={editor.wordWrap}
onChange={(wordWrap) => setEditor({ wordWrap })}
/>
</SettingRow>
<SettingRow label="Line numbers">
<SegmentedControl
label="Line numbers"
options={ONOFF_OPTIONS}
value={editor.lineNumbers}
onChange={(lineNumbers) => setEditor({ lineNumbers })}
/>
</SettingRow>
<SettingRow label="Tab size" htmlFor="set-tabsize">
<NumberControl
id="set-tabsize"
min={1}
max={8}
value={editor.tabSize}
onChange={(tabSize) => setEditor({ tabSize })}
/>
</SettingRow>
<SettingFooter>
<ResetButton onClick={resetEditor}>Reset editor defaults</ResetButton>
</SettingFooter>
</SettingsPopover>
);
}
// Register the bundled Vega-Lite schema once: resolves `$schema` locally (no
// network warning) and powers validation, autocomplete, and hover docs.
configureVegaLiteJson();
@@ -132,6 +222,7 @@ function EditorToolbar() {
>
Publish
</button>
<EditorSettings />
</div>
);
}
@@ -144,21 +235,26 @@ export function SpecEditor() {
const bufferEpoch = useSnippetStore((s) => s.bufferEpoch);
const uiTheme = useAppStore((s) => s.uiTheme);
const error = usePreviewStore((s) => s.error);
// Editor preferences (spec §07 → Editor); applied live below as they change.
const editorPrefs = useUserSettingsStore((s) => s.saved.editor);
// Create the editor once, on mount.
useEffect(() => {
if (!hostRef.current) return;
// Seed from the user's current editor settings so the first paint matches.
const ed = useUserSettingsStore.getState().saved.editor;
const editor = monaco.editor.create(hostRef.current, {
value: selectShownText(useSnippetStore.getState()),
language: 'json',
automaticLayout: true,
minimap: { enabled: false },
minimap: { enabled: ed.minimap },
// The editor is a Plex Mono surface per the design language (doc §3.1).
// Monaco needs an explicit family string — it can't read the CSS token.
fontFamily: "'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace",
fontSize: 13,
tabSize: 2,
wordWrap: 'on',
fontSize: ed.fontSize,
tabSize: ed.tabSize,
wordWrap: ed.wordWrap,
lineNumbers: ed.lineNumbers,
folding: true,
showFoldingControls: 'always', // keep fold arrows visible, not only on hover
scrollBeyondLastLine: false,
@@ -204,10 +300,27 @@ export function SpecEditor() {
});
}, [bufferEpoch, editorView, activeId]);
// Editor theme follows the UI theme.
// Apply editor preferences live as they change (spec §07 Apply → takes effect
// immediately). tabSize is a model option, so it's set on the model.
useEffect(() => {
monaco.editor.setTheme(uiTheme === 'dark' ? 'vs-dark' : 'vs');
}, [uiTheme]);
const editor = editorRef.current;
if (!editor) return;
editor.updateOptions({
fontSize: editorPrefs.fontSize,
wordWrap: editorPrefs.wordWrap,
lineNumbers: editorPrefs.lineNumbers,
minimap: { enabled: editorPrefs.minimap },
});
editor.getModel()?.updateOptions({ tabSize: editorPrefs.tabSize });
}, [editorPrefs]);
// Editor theme: Auto follows the app UI theme; an explicit override forces one
// (spec §07 → Editor theme, provisional).
useEffect(() => {
const pref = editorPrefs.theme;
const effective = pref === 'auto' ? uiTheme : pref;
monaco.editor.setTheme(effective === 'dark' ? 'vs-dark' : 'vs');
}, [uiTheme, editorPrefs.theme]);
return (
<div className={styles.editorPane}>