Layout System
Opening panels
ts
// Docked (default) — goes into the first available leaf group
client.openPanel('my-map', 'map', { title: 'Satellite View' });
// Forced docked — explicit
client.openPanel('my-map', 'map', { initialTarget: 'docked' });
// Floating — opens as a resizable window
client.openPanel('settings', 'settings', { initialTarget: 'floating' });
// Tabbed alongside an existing panel
client.openPanel('log-2', 'logs', { initialTarget: 'tabbed' });If the panel ID is already open, openPanel focuses it instead of creating a duplicate.
Activating / focusing panels
ts
client.focusPanel('my-map');
// For floating: raises z-index so the window appears on top.
// For docked: selects the tab within its leaf group.Querying open panels
ts
if (!client.isOpen('my-map')) {
client.openPanel('my-map', 'map');
}
const openIds = client.getOpenPanelIds();
// → ['my-map', 'settings', 'log-2']Floating window operations
ts
client.floatPanel('my-map'); // detach from grid
client.floatPanel('my-map', { x: 100, y: 60, width: 800, height: 600 });
client.maximizePanel('my-map'); // fill the workspace
client.dockPanel('my-map'); // return to grid
client.dockPanel('my-map', 'left-leaf'); // dock to specific groupMinimizing
ts
client.minimizePanel('my-map'); // sends to taskbar
client.restorePanel('my-map'); // restores from taskbarLayout serialization
ts
// Save the entire workspace to JSON
const snapshot = client.saveLayout();
localStorage.setItem('layout', snapshot);
// Restore — replaces everything currently open
client.loadLayout(localStorage.getItem('layout') ?? '');The snapshot JSON contains:
gridRoot— the full tree of branches and leaf groupsfloating— positions and sizes of floating windowsminimized— panels in the taskbarpanels— metadata (title, component key, state) for every open panel
Pre-loading a layout on startup
Pass the JSON string directly to WorkspaceClient.initialState:
ts
const client = new WorkspaceClient({
panels: { ... },
initialState: localStorage.getItem('layout'),
});The layout is parsed synchronously before the first render — no flicker, no useEffect needed.
Loading asynchronously
For layouts fetched from a server, use the imperative API:
ts
// Outside React (after provider mounts):
fetchLayoutFromServer().then(json => client.loadLayout(json));
// Or inside a component:
useEffect(() => {
fetch('/api/layout').then(r => r.text()).then(json => {
actions.loadLayout(json);
});
}, []);Dirty-state close guards
Prevent accidental data loss with close guards:
ts
// In your panel component:
const actions = useWindowManagerActions();
useEffect(() => {
actions.registerCloseGuard('editor-1', () => {
return !hasUnsavedChanges || confirm('Discard unsaved changes?');
});
return () => actions.unregisterCloseGuard('editor-1');
}, [hasUnsavedChanges]);
// Or use the built-in dirty flag + modal:
actions.setPanelDirty('editor-1', true, {
title: 'Unsaved Changes',
body: 'Your changes will be lost. Continue?',
});Event bus
ts
// Publish from any panel or outside React:
client.publish('map:zoom', { level: 12 });
actions.publish('selection:changed', { ids: ['feature-1'] });
// Subscribe in a panel:
const actions = useWindowManagerActions();
useEffect(() => {
return actions.subscribe('map:zoom', ({ level }) => {
setZoom(level);
});
}, []);