mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Editor: interactive composition wireframe — drag to reorder and restructure
Drag a view's box (or Alt+up/down) to reorder it within its container; drag onto another view's edge to pair the two in a new row/column, move across containers, or insert. Each leaf shows a glyph of its mark type. Core: spec-restructure.wrapViews (wrap, with flatten-to-insert, collapse, and data-pin invariants) and spec-insert.moveViewTo; the editor applies every drag as one undoable edit via AppStore.composeRequest, keeping the editor the single text source.
This commit is contained in:
@@ -38,8 +38,10 @@ import {
|
||||
elementOffset,
|
||||
insertView,
|
||||
moveView,
|
||||
moveViewTo,
|
||||
type SpecPath,
|
||||
} from '@core/spec-insert';
|
||||
import { wrapViews, type DropAxis } from '@core/spec-restructure';
|
||||
import {
|
||||
unwrapSingleton,
|
||||
wrapInConcat,
|
||||
@@ -176,16 +178,22 @@ const WRAP_NOUN: Record<WrapKind, string> = {
|
||||
repeat: 'a repeat',
|
||||
};
|
||||
|
||||
/** Replace the scope as one undoable edit (the toolbar / palette path). */
|
||||
/**
|
||||
* Replace the scope as one undoable edit (the toolbar / palette path). `focus`
|
||||
* returns focus to the editor afterwards — true for editor-originated actions,
|
||||
* false when the wireframe drove the edit and must keep its own focus (APG: a
|
||||
* keyboard reorder stays on the moved item for consecutive moves).
|
||||
*/
|
||||
function writeBack(
|
||||
editor: monaco.editor.IStandaloneCodeEditor,
|
||||
range: monaco.Range,
|
||||
text: string,
|
||||
focus = true,
|
||||
): void {
|
||||
editor.pushUndoStop();
|
||||
editor.executeEdits('spec-transform', [{ range, text }]);
|
||||
editor.pushUndoStop();
|
||||
editor.focus();
|
||||
if (focus) editor.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,13 +253,18 @@ export function runUnwrap(editor: monaco.editor.IStandaloneCodeEditor): void {
|
||||
* and a repeated click keeps acting on the same view. `followIndex` is the view's
|
||||
* index in `arrayPath` *after* the edit. Reformatted in the app's compact style,
|
||||
* which is idempotent on an already-formatted draft.
|
||||
*
|
||||
* `successTitle` of null suppresses the success toast (the wireframe gives its own
|
||||
* feedback — visible move + live-region announcement — so a per-move toast would
|
||||
* double up); `focusEditor` of false keeps focus off the editor for the same path.
|
||||
*/
|
||||
function applyArrayEdit(
|
||||
editor: monaco.editor.IStandaloneCodeEditor,
|
||||
build: (spec: JsonObject) => JsonObject | null,
|
||||
arrayPath: SpecPath,
|
||||
followIndex: number,
|
||||
successTitle: string,
|
||||
successTitle: string | null,
|
||||
focusEditor = true,
|
||||
): void {
|
||||
const model = editor.getModel();
|
||||
if (!model) return;
|
||||
@@ -267,14 +280,15 @@ function applyArrayEdit(
|
||||
return;
|
||||
}
|
||||
const formatted = formatScoped(model, wholeDocument(model), next);
|
||||
writeBack(editor, model.getFullModelRange(), formatted);
|
||||
writeBack(editor, model.getFullModelRange(), formatted, focusEditor);
|
||||
const offset = elementOffset(formatted, arrayPath, followIndex);
|
||||
if (offset !== null) {
|
||||
const pos = model.getPositionAt(offset);
|
||||
editor.setPosition(pos);
|
||||
editor.revealPositionInCenterIfOutsideViewport(pos);
|
||||
}
|
||||
notify({ kind: 'success', title: successTitle, message: 'Undo with ⌘/Ctrl+Z.' });
|
||||
if (successTitle)
|
||||
notify({ kind: 'success', title: successTitle, message: 'Undo with ⌘/Ctrl+Z.' });
|
||||
}
|
||||
|
||||
/** Insert an empty view at `index` of the composition at `arrayPath`. */
|
||||
@@ -302,6 +316,55 @@ function runMoveView(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder the view at `from` to `to` within the composition at `arrayPath` — the
|
||||
* composition wireframe's drag/keyboard reorder. No success toast and no editor
|
||||
* focus-steal: the wireframe owns the feedback and keeps focus on the moved box.
|
||||
*/
|
||||
export function runMoveViewTo(
|
||||
editor: monaco.editor.IStandaloneCodeEditor,
|
||||
arrayPath: SpecPath,
|
||||
from: number,
|
||||
to: number,
|
||||
): void {
|
||||
applyArrayEdit(editor, (s) => moveViewTo(s, arrayPath, from, to), arrayPath, to, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restructure the spec by pairing the dragged `sourcePath` view beside the drop
|
||||
* `targetPath` view in a new concat of `axis` — the wireframe's cross-container
|
||||
* drag (wrap, move-in, collapse, all in core/spec-restructure). One undoable edit;
|
||||
* no toast and no editor focus-steal, as with the reorder path. A degenerate or
|
||||
* stale drop surfaces an info toast rather than silently doing nothing.
|
||||
*/
|
||||
export function runWrapViews(
|
||||
editor: monaco.editor.IStandaloneCodeEditor,
|
||||
targetPath: SpecPath,
|
||||
sourcePath: SpecPath,
|
||||
axis: DropAxis,
|
||||
side: 'before' | 'after',
|
||||
): void {
|
||||
const model = editor.getModel();
|
||||
if (!model) return;
|
||||
const spec = parseSpecObject(model.getValue());
|
||||
if (!spec) return;
|
||||
const next = wrapViews(spec, targetPath, sourcePath, axis, side);
|
||||
if (!next) {
|
||||
notify({
|
||||
kind: 'info',
|
||||
title: 'Could not restructure',
|
||||
message: 'That drop isn’t possible here — the composition may have changed.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
writeBack(
|
||||
editor,
|
||||
model.getFullModelRange(),
|
||||
formatScoped(model, wholeDocument(model), next),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/** Insert a view above/below the one the cursor is in (the keyboard path). */
|
||||
function runInsertRelative(
|
||||
editor: monaco.editor.IStandaloneCodeEditor,
|
||||
|
||||
Reference in New Issue
Block a user