Workspace Panes — cdx_panes
cdx_panes is the IDE-style workspace shell used by the Vyuh CDX applications. It provides a Workspace widget that composes a header bar, navigation rail, three resizable docks (left / right / bottom), a body area with optional tabs, and a status bar — all driven by a single WorkspaceController with a JSON-serializable layout state.
The package also exports SplitPane and MultiSplitPane, the lower-level resizable layout primitives used by the workspace itself. Use those when you need only a local two-pane or N-pane split without the full IDE shell.
import 'package:cdx_panes/cdx_panes.dart';Workspace
Workspace(
controller: workspaceController,
theme: const WorkspaceTheme(),
headerBar: HeaderBar(title: const Text('My App'), trailing: const _UserMenu()),
statusBar: const StatusBar(items: [...]),
navigation: NavigationBar(entries: [...]),
leftDock: Dock(
placement: DockPlacement.left,
panes: [
Pane(id: 'explorer', title: 'Explorer', icon: FluentIcons.folder_24_regular, builder: (_) => const ExplorerPane()),
Pane(id: 'search', title: 'Search', icon: FluentIcons.search_24_regular, builder: (_) => const SearchPane()),
],
),
rightDock: Dock(placement: DockPlacement.right, panes: [...]),
bottomDock: Dock(placement: DockPlacement.bottom, panes: [...]),
body: WorkspaceBody(child: const EditorArea(), tabs: BodyTabs(items: [...])),
)Dock + Pane
A Dock configures one of the three placements (left / right / bottom). Each dock owns one or more Panes, and the user switches between them via the dock's tab strip.
| Dock param | Purpose |
|---|---|
placement | DockPlacement.left / .right / .bottom |
panes | List of Pane |
defaultActivePaneId | Pane to activate initially (defaults to first) |
trailingStripAction | Widget appended to the trailing end of the strip |
resizable | Whether the user can drag the edge (default true) |
tabBarVisibility | auto / visible / hidden (default auto — hidden when single pane) |
defaultMode | DockMode.expanded / .collapsed / .hidden / .floating |
defaultSize / minSize / maxSize | Pixel sizes |
defaultSizeFraction / minSizeFraction / maxSizeFraction | Viewport-relative 0..1 — pairs with pixel values; the more permissive ceiling wins so docks grow on wider viewports |
Dock(
placement: DockPlacement.right,
defaultSize: 360,
defaultSizeFraction: 0.30, // 30% of viewport, but floored at 360 px
panes: [
Pane(
id: 'inspector',
title: 'Inspector',
icon: FluentIcons.options_24_regular,
builder: (_) => const InspectorPane(),
headerActionsBuilder: (_) => Button.icon(icon: FluentIcons.pin_24_regular, ...),
badge: const StatusBadge.compact(Colors.amber, label: 'Beta'),
closable: true,
),
],
)DockMode
enum DockMode {
hidden, // not rendered at all
collapsed, // tab strip only, content hidden
expanded, // strip + content, body shrinks (default)
floating, // content overlays body without consuming flex space
}floating is for transient detail presentations (create/edit forms, focused inspectors) that should dismiss via navigation rather than user dock manipulation. The body keeps its full size; a non-interactive scrim covers it.
NavigationBar
NavigationBar(
entries: [
NavigationItem(id: 'inbox', label: 'Inbox', icon: FluentIcons.mail_inbox_24_regular, onTap: () => ...),
NavigationItem(id: 'lots', label: 'Lots', icon: FluentIcons.box_24_regular, ...),
const NavigationDivider(),
NavigationGroup(label: 'Reports', children: [
NavigationItem(id: 'reports.daily', label: 'Daily', icon: ...),
NavigationItem(id: 'reports.monthly', label: 'Monthly', icon: ...),
]),
],
)NavigationEntry is sealed:
| Variant | Purpose |
|---|---|
NavigationItem | Tappable row (icon + label + optional badge / trailing) |
NavigationGroup | Collapsible section |
NavigationDivider | Visual separator |
The bar's mode is driven by the controller (NavigationBarMode.hidden / .drawer / .pinnedCompact / .pinnedExpanded).
WorkspaceController
Owns dock runtime state (mode, size, active pane id) and exposes MobX observables. Read state via Observer; mutate via methods.
final controller = WorkspaceController(
navigation: navigationBar,
leftDock: leftDock,
rightDock: rightDock,
bottomDock: bottomDock,
);
controller.setDockMode(DockPlacement.left, DockMode.collapsed);
controller.activatePane(DockPlacement.right, 'inspector');
controller.resizeDock(DockPlacement.bottom, 240);
controller.setNavigationMode(NavigationBarMode.pinnedCompact);
controller.resizeNavigation(280);Layout persistence
WorkspaceConfig is the JSON-serializable snapshot — persist on the client (or server), restore on next launch.
final config = controller.toConfig(); // WorkspaceConfig
final json = config.toJson();
await prefs.setString('workspace.layout', jsonEncode(json));
// later
final restored = WorkspaceConfig.fromJson(jsonDecode(stored));
controller.loadConfig(restored); // partial applicationBuilt-in SharedPreferencesLayoutStorage adapter:
controller.attachStorage(SharedPreferencesLayoutStorage(prefs, key: 'workspace.layout'));
// auto-saves on dock / nav changesLayoutStorage is abstract — provide your own implementation for custom backends (server-side persistence, in-memory test doubles).
WorkspaceTheme
Visual styling tokens (sizes, icons, resizer thickness). Defaults are JetBrains-tight; override any subset to retune the workspace.
Workspace(
...,
theme: WorkspaceTheme(
workspaceBackground: scheme.surface,
resizerThickness: 6,
iconSize: 18,
stripThickness: 36,
),
)WorkspaceScope
WorkspaceScope.of(context) exposes the controller, layout size, and theme to any descendant. Use it in custom panes that need to react to layout-size changes (small / medium / large) or trigger workspace mutations.
SplitPane / MultiSplitPane
SplitPane is a two-pane convenience wrapper around MultiSplitPane. It owns local drag state and reports ratio changes to the caller.
SplitPane(
axis: Axis.horizontal,
initialRatio: 0.35,
minFirstSize: 280,
minSecondSize: 420,
onRatioChanged: (ratio) => settings.saveExplorerRatio(ratio),
first: const ExplorerPane(),
second: const EditorPane(),
)Use MultiSplitPane when you have three or more children, need controlled pixel sizes, or want to disable individual handles.
MultiSplitPane(
axis: Axis.horizontal,
sizes: savedSizes,
resizableHandles: const [true, false],
onSizesChanged: settings.saveInspectorSizes,
children: const [
SplitPaneChild(weight: 1, minSize: 240, child: ExplorerPane()),
SplitPaneChild(weight: 2, minSize: 400, child: EditorPane()),
SplitPaneChild(weight: 1, minSize: 280, child: InspectorPane()),
],
)MultiSplitPane requires bounded width for horizontal splits or bounded height for vertical splits. If sizes is provided, it is treated as the controlled pixel-size source and fitted to the current extent; otherwise weights seed the initial sizes and drag state is retained locally.
Cross-links
- Layout & Navigation —
MasterDetailScaffoldis the in-page alternative for simpler shells - Panels —
CdxSlideInPanelfor transient drilldown overlays