mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Add distributed settings and workspace import/export (M5)
This commit is contained in:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user