Skip to content

Architecture ​

The Vyuh Dashboard Editor follows a layered architecture with a reactive MobX-based engine at its core, a command pattern for all mutations, and a pluggable extension system for cross-cutting concerns.

System Overview ​

Layered Architecture ​

DashboardEngine ​

The DashboardEngine is the core state kernel. It owns the reactive layout, executes commands, and emits events to extensions. It has no knowledge of UI, persistence, or history -- those are handled by extensions.

dart
class DashboardEngine implements DashboardEngineApi {
  // Reactive layout state
  late final Observable<DashboardLayout> layoutObservable;
  DashboardLayout get layout => layoutObservable.value;

  // Extension management
  void addExtension(DashboardExtension extension);
  void removeExtension(String id);
  E? getExtension<E extends DashboardExtension>();

  // Command execution
  Future<void> executeCommand(DashboardCommand command);
  Future<void> applyUndo(DashboardCommand command);
  Future<void> applyRedo(DashboardCommand command);

  // Direct layout replacement (for persistence loads)
  void replaceLayout(DashboardLayout newLayout);

  // Event emission
  void emitEvent(DashboardEvent event);

  // Batch operations
  void batch(String reason, void Function() operations);
}

Command Execution Flow ​

When the engine executes a command, it follows this precise sequence:

All mutations happen inside a MobX runInAction, which ensures that UI rebuilds are batched. The engine emits two events per command execution: DashboardLayoutChanged (with previous and current layout) and DashboardCommandExecuted (with the command itself).

Engine API ​

The engine exposes a high-level mutation API that creates and executes commands internally:

MethodCommand CreatedDescription
addRow()AddRowCommandAdd a row with a single empty column
removeRow(rowId)RemoveRowCommandRemove a row and all its columns
reorderRow(old, new)ReorderRowsCommandMove a row to a new position
splitRow(rowId, n)SplitRowCommandReplace columns with n equal columns
addColumn(rowId)AddColumnCommandAdd an empty column to a row
removeColumn(rowId, colId)RemoveColumnCommandRemove a column from a row
reorderColumns(rowId, old, new)ReorderColumnsCommandReorder columns within a row
updateColumnFlex(...)UpdateColumnFlexCommandChange a single column's flex
resizeAdjacentColumns(...)ResizeAdjacentColumnsCommandResize two adjacent columns together
updateRowFlex(...)UpdateRowFlexCommandUpdate flex for all columns in a row
updateRowTitle(...)UpdateRowTitleCommandChange or clear row title
updateRowHeight(...)UpdateRowHeightCommandSet row height in pixels
updateRowViewportFraction(...)UpdateRowViewportFractionCommandSet viewport-relative row height
addComponent(...)AddComponentCommandPlace a component in a column
removeComponent(...)RemoveComponentCommandClear a column's component
replaceComponent(...)ReplaceComponentCommandReplace one component with another
swapComponents(...)SwapComponentsCommandSwap components between two columns
duplicateComponent(...)DuplicateComponentCommandCopy a component to the nearest empty slot
updateComponentConfig(...)UpdateComponentConfigCommandUpdate a component's properties
updateLayoutMetadata(...)UpdateDashboardMetadataCommandPatch dashboard-level metadata

Command Pattern ​

Every mutation is a DashboardCommand that implements two methods:

dart
abstract class DashboardCommand {
  DashboardLayout execute();  // Apply the mutation
  DashboardLayout undo();     // Reverse the mutation
  String get description;     // Human-readable label
}

Commands are pure functions from layout to layout. They capture the current layout in their constructor and return a new layout from execute() and undo(). This makes them trivially testable:

dart
final command = AddRowCommand(
  layout: currentLayout,
  rowToAdd: newRow,
  position: 0,
);

final afterExecute = command.execute();
expect(afterExecute.rows.length, currentLayout.rows.length + 1);

final afterUndo = command.undo();
expect(afterUndo.rows.length, currentLayout.rows.length);

Command Categories ​

The 20 built-in commands fall into four categories:

Row commands: AddRowCommand, RemoveRowCommand, ReorderRowsCommand, SplitRowCommand, UpdateRowTitleCommand, UpdateRowHeightCommand, UpdateRowViewportFractionCommand

Column commands: AddColumnCommand, RemoveColumnCommand, ReorderColumnsCommand, UpdateColumnFlexCommand, ResizeAdjacentColumnsCommand, UpdateRowFlexCommand

Component commands: AddComponentCommand, RemoveComponentCommand, ReplaceComponentCommand, SwapComponentsCommand, DuplicateComponentCommand, UpdateComponentConfigCommand

Dashboard commands: UpdateDashboardMetadataCommand

CommandHistory ​

The CommandHistory class manages the undo/redo stack:

dart
class CommandHistory {
  final int maxHistorySize;  // Default: 50

  void execute(DashboardCommand command);  // Add to stack
  DashboardCommand? undo();   // Move back
  DashboardCommand? redo();   // Move forward
  void clear();

  bool get canUndo;
  bool get canRedo;
  int get historySize;
  int get currentPosition;
}

When a new command is executed after an undo, the redo stack is discarded (standard undo/redo behavior). The history has a configurable maximum size (default 50) to prevent unbounded memory growth.

Event System ​

The engine communicates with extensions through structured events:

dart
abstract class DashboardEvent {
  const DashboardEvent();
}

// Emitted on every layout change
class DashboardLayoutChanged extends DashboardEvent {
  final DashboardLayout previous;
  final DashboardLayout current;
}

// Emitted when a command executes
class DashboardCommandExecuted extends DashboardEvent {
  final DashboardCommand command;
}

// Emitted when a command is undone
class DashboardCommandUndone extends DashboardEvent {
  final DashboardCommand command;
}

// Emitted when a command is redone
class DashboardCommandRedone extends DashboardEvent {
  final DashboardCommand command;
}

// Batch boundaries
class DashboardBatchStarted extends DashboardEvent {
  final String reason;
}
class DashboardBatchEnded extends DashboardEvent {}

Events are dispatched synchronously to all registered extensions in registration order. This means extensions can rely on seeing events in a consistent sequence.

Extension System ​

Extensions augment the engine without modifying its core. Each extension implements the DashboardExtension interface:

dart
abstract class DashboardExtension {
  String get id;                          // Unique identifier
  void attach(DashboardEngineApi engine); // Called when added
  void detach();                          // Called when removed
  void onEvent(DashboardEvent event);     // React to events
}

The DashboardEngineApi is a forward declaration that provides extensions with access to the layout, command execution, and event emission without creating circular dependencies.

Built-in Extensions ​

ExtensionIDPurpose
DashboardHistoryExtensiondashboard-historyUndo/redo via CommandHistory
DashboardPersistenceExtensiondashboard-persistenceLayout JSON serialization, direct layout load, dirty tracking
DashboardSelectionExtensiondashboard-selectionCell, row, and component selection state
DashboardAnalyticsExtensiondashboard-analyticsTrack component additions and editor actions

Extension Event Handling ​

Each extension listens for specific events:

  • History listens for DashboardCommandExecuted to record commands in the stack.
  • Persistence listens for DashboardCommandExecuted, DashboardCommandUndone, and DashboardCommandRedone to set the dirty flag.
  • Selection listens for DashboardLayoutChanged to validate that selected rows/columns still exist.
  • Analytics listens for DashboardCommandExecuted to track component additions.

DashboardEditorController ​

The DashboardEditorController is a facade that composes the engine with all four extensions and provides the complete editing API:

dart
class DashboardEditorController {
  final DashboardEngine engine;

  // Extensions
  late final DashboardHistoryExtension history;
  late final DashboardPersistenceExtension persistence;
  late final DashboardSelectionExtension selection;
  late final DashboardAnalyticsExtension analytics;

  // Observable state
  Observable<DashboardLayout> get layout;
  Observable<bool> get isDirty;
  Observable<String?> get selectedCellRowId;
  Observable<String?> get selectedCellColumnId;

  // Layout mutations (delegates to engine)
  Future<String> addRow({int? position});
  Future<void> removeRow(String rowId);
  Future<void> splitRow(String rowId, int numColumns);
  // ... all engine methods

  // Selection
  void selectCell(String rowId, String columnId);
  void clearSelection();
  void navigateCell({int dx, int dy});

  // Undo/redo
  Future<void> undo();
  Future<void> redo();

  // Persistence / serialization
  Map<String, dynamic> toJson();
  void fromJson(Map<String, dynamic> json);
  void loadLayout(DashboardLayout newLayout);
  void markClean();
  void markDirty();
}

The controller also manages editor-specific draft state for the config panel, ensuring property edits are committed back to the layout through UpdateComponentConfigCommand rather than mutating the component instance in place.

Next Steps ​