Editor: Extract Config to New Theme as a third Config action

This commit is contained in:
2026-06-12 18:16:15 +03:00
parent b193464f55
commit 0baac3c64b
4 changed files with 72 additions and 5 deletions
+8 -1
View File
@@ -28,6 +28,7 @@ import { openModal } from '../modals/ModalCoordinator';
import {
installSpecConfigActions,
runExtractConfig,
runExtractConfigToTheme,
runMergeChartTheme,
} from '../services/spec-config-actions';
import { useAppStore } from '../stores/AppStore';
@@ -160,6 +161,11 @@ const CONFIG_ACTIONS = [
label: 'Extract config from spec',
detail: 'Remove the config block and copy it to the clipboard',
},
{
value: 'extract-theme',
label: 'Extract config to new theme',
detail: 'Save the config block as a custom chart theme and select it',
},
] as const;
type ConfigActionId = (typeof CONFIG_ACTIONS)[number]['value'];
@@ -194,7 +200,8 @@ function EditorToolbar({
const editor = editorRef.current;
if (!editor) return;
if (action === 'merge') runMergeChartTheme(editor);
else void runExtractConfig(editor);
else if (action === 'extract') void runExtractConfig(editor);
else runExtractConfigToTheme(editor);
};
const handleRevert = async () => {
+61 -2
View File
@@ -10,6 +10,9 @@
* it to the clipboard — for cleaning baked-in styling out of a pasted spec.
* The clipboard write happens *before* the edit, so a clipboard failure
* never destroys the only copy.
* - **Extract config to new theme** removes the block and saves it as a custom
* chart theme instead, selecting it — the one-step "this pasted spec's
* styling should be a reusable theme" path.
*
* Both replace the document via `executeEdits`, so ⌘Z restores the previous
* text. Both no-op (with a toast naming the reason) on invalid JSON; Monaco's
@@ -26,10 +29,16 @@
import * as monaco from 'monaco-editor/esm/vs/editor/edcore.main';
import { formatJson } from '@core/json-format';
import { extractConfigFromSpec, isJsonObject, mergeConfigIntoSpec } from '@core/spec-config';
import { CHART_THEME_OPTIONS, chartConfigForSelection } from '@core/vega-themes';
import {
CHART_THEME_OPTIONS,
chartConfigForSelection,
customThemeSelection,
} from '@core/vega-themes';
import { copyText } from '../infrastructure/file-transfer';
import { useAppStore } from '../stores/AppStore';
import { useCustomThemeStore } from '../stores/CustomThemeStore';
import { notify } from '../stores/NotificationStore';
import { selectActiveSnippet, useSnippetStore } from '../stores/SnippetStore';
/** Parse the model's JSON, or toast (and return null) when it isn't a JSON object. */
function parseSpecObject(model: monaco.editor.ITextModel): Record<string, unknown> | null {
@@ -140,7 +149,47 @@ export async function runExtractConfig(editor: monaco.editor.IStandaloneCodeEdit
}
/**
* Register both actions on the editor (context menu + F1 palette — the expert
* Turn the draft's `config` block into a saved custom theme: create the theme
* (named after the snippet, auto-suffixed if taken), remove the block from the
* spec, and select the new theme as the active chart theme — so the styling
* keeps applying to this chart, now from the injected side of the boundary.
* (Not pixel-identical when another theme was active: that theme's
* non-overlapping keys stop applying once the new theme replaces it.)
*/
export function runExtractConfigToTheme(editor: monaco.editor.IStandaloneCodeEditor): void {
const model = editor.getModel();
if (!model) return;
const spec = parseSpecObject(model);
if (!spec) return;
const { spec: rest, config } = extractConfigFromSpec(spec);
if (config === null) {
notify({
kind: 'info',
title: 'No config to extract',
message: 'This spec has no config block.',
});
return;
}
const snippet = selectActiveSnippet(useSnippetStore.getState());
const theme = useCustomThemeStore
.getState()
.createTheme(snippet ? `${snippet.name} theme` : 'Extracted theme', config);
useAppStore.getState().setChartTheme(customThemeSelection(theme.id));
replaceDocument(editor, model, 'extract-config-to-theme', rest);
notify({
kind: 'success',
title: 'Theme created',
message:
`The config block became the custom theme “${theme.name}”, now selected as the ` +
'chart theme. Rename or refine it via Edit themes…; undo the spec edit with ⌘/Ctrl+Z.',
});
}
/**
* Register the config actions on the editor (context menu + F1 palette — the expert
* accelerators; the toolbar Config menu is the discoverable home). Returns a
* disposable that detaches them (dispose on editor unmount, like the
* format-on-paste hook).
@@ -166,10 +215,20 @@ export function installSpecConfigActions(
run: () => runExtractConfig(editor),
});
const extractToTheme = editor.addAction({
id: 'astrolabe.extract-config-to-theme',
label: 'Extract Config to New Theme',
contextMenuGroupId: 'astrolabe',
contextMenuOrder: 3,
precondition: '!editorReadonly',
run: () => runExtractConfigToTheme(editor),
});
return {
dispose() {
merge.dispose();
extract.dispose();
extractToTheme.dispose();
},
};
}
+1 -1
View File
@@ -11,7 +11,7 @@
* changes how the chart looks.
* - **Extract** lifts the `config` block out of a spec — for cleaning styling
* out of a pasted-in spec (the caller decides where the extracted config
* goes: clipboard today, a saved theme later).
* goes: the clipboard, or a saved custom theme).
*
* Pure object-in/object-out; JSON text handling (parse, format, undo) is the
* editor integration's job.