WindowManager
WindowManager lays out a set of windows on a fixed slot grid. Windows dock into slots, can be dragged onto other slots to move or swap, and can be undocked into free-floating, resizable windows.
import { WindowManager } from 'jarvis-react-ui';
Slot grid
The grid has nine slots: three in the left column (L1–L3), three in the right column
(R1–R3) and three in the bottom strip (B1–B3). The columns are 316px wide, the bottom strip
is 140px high, and the top 60px stay free for a TopBar. The grid is computed
from the size of the WindowManager, which fills its containing block (position: fixed; inset: 0):
the viewport, or the nearest ancestor with a transform. Column slots share the height left
between the top bar and the bottom strip (at least 60px each), so the 420px demo stages below dock
their windows in the bottom strip.
Basic
assignments maps window ids to slots and is controlled: keep it in state and update it in
onAssignmentsChange. Drag a window header onto another slot to move it, or onto an occupied slot to
swap the two windows.
import { useState } from 'react';
import { WindowManager } from 'jarvis-react-ui';
import type { ManagedWindow, SlotId } from 'jarvis-react-ui';
const WINDOWS: ManagedWindow[] = [
{ id: 'system', title: 'System', ix: '◈', itemRenderer: () => 'CPU 42% · RAM 61%' },
{ id: 'transcript', title: 'Transcript', ix: '▸', itemRenderer: () => 'Listening…' },
{ id: 'agenda', title: 'Agenda', ix: '▦', itemRenderer: () => '09:30 · Standup' },
];
export default function Basic() {
const [assignments, setAssignments] = useState<Record<string, SlotId>>({
system: 'B1',
transcript: 'B2',
agenda: 'B3',
});
return (
<WindowManager
windows={WINDOWS}
assignments={assignments}
onAssignmentsChange={setAssignments}
/>
);
}
Compact and expanded
Every window has a mode. compact windows are docked in their slot. Click the undock button in the
header, or double-click the header, to switch to expanded: the window floats above the grid, larger
than its slot, and can be dragged freely and resized from its edges and corners. The dock button
switches back.
itemRenderer receives { mode, focused, dragging }, so a window can show a compact summary while
docked and more detail while expanded.
import { useState } from 'react';
import { Metric, Sparkline, WindowManager } from 'jarvis-react-ui';
import type { ManagedWindow, SlotId } from 'jarvis-react-ui';
const WINDOWS: ManagedWindow[] = [
{
id: 'cpu',
title: 'CPU',
badge: 'LIVE',
itemRenderer: ({ mode }) =>
mode === 'compact' ? (
<Metric value={42} unit="%" small />
) : (
<div className="flex flex-col gap-3">
<Metric value={42} unit="%" />
<Sparkline data={[30, 42, 38, 51, 47, 42, 45, 39]} />
</div>
),
},
];
export default function Modes() {
const [assignments, setAssignments] = useState<Record<string, SlotId>>({ cpu: 'B2' });
const [focusedId, setFocusedId] = useState<string | null>(null);
return (
<WindowManager
windows={WINDOWS}
assignments={assignments}
onAssignmentsChange={setAssignments}
focusedId={focusedId}
onFocusChange={setFocusedId}
/>
);
}
Controlled state
focusedId and modes can be controlled too. Focus moves to a window when it is pressed, dragged or
toggled, and onFocusChange receives null on a pointer down outside every window. Omit modes to
let WindowManager track modes itself; onModesChange fires either way.
import { useState } from 'react';
import { Mono, WindowManager } from 'jarvis-react-ui';
import type { ManagedWindow, PanelMode, SlotId } from 'jarvis-react-ui';
const WINDOWS: ManagedWindow[] = [
{ id: 'system', title: 'System', ix: '◈', itemRenderer: ({ mode }) => mode },
{ id: 'agenda', title: 'Agenda', ix: '▦', itemRenderer: ({ mode }) => mode },
];
export default function Controlled() {
const [assignments, setAssignments] = useState<Record<string, SlotId>>({
system: 'B1',
agenda: 'B3',
});
const [focusedId, setFocusedId] = useState<string | null>('system');
const [modes, setModes] = useState<Record<string, PanelMode>>({});
return (
<>
<Mono size="sm" secondary>
focus: {focusedId ?? 'none'} · slots:{' '}
{Object.entries(assignments)
.map(([id, slot]) => `${id}→${slot}`)
.join(' ')}{' '}
· modes: {WINDOWS.map((w) => `${w.id}→${modes[w.id] ?? 'compact'}`).join(' ')}
</Mono>
<WindowManager
windows={WINDOWS}
assignments={assignments}
onAssignmentsChange={setAssignments}
focusedId={focusedId}
onFocusChange={setFocusedId}
modes={modes}
onModesChange={setModes}
/>
</>
);
}
Reset
The reset button in each header docks the window, clears any custom size and moves it back to its
home slot. Home slots come from homeAssignments; changes to the prop are used by the next reset.
Without homeAssignments, the assignments of the first render are the home slots.
Controlled expanded rects
Pass expandedRects to own the positions and sizes of expanded windows, e.g. to persist them.
WindowManager then stops storing them and reports every change through onExpandedRectsChange:
the default rect when a window is undocked, the final position when a drag ends, each step of a
resize and the removal on reset. Apply the map you receive, otherwise the window snaps back.
onExpandedRectsChange also fires when expandedRects is omitted.
import { useState } from 'react';
import { Mono, WindowManager } from 'jarvis-react-ui';
import type { ExpandedRect, ManagedWindow, SlotId } from 'jarvis-react-ui';
const WINDOWS: ManagedWindow[] = [
{ id: 'map', title: 'Map', ix: '◎', itemRenderer: () => 'Undock, then drag or resize me.' },
];
export default function ControlledRects() {
const [assignments, setAssignments] = useState<Record<string, SlotId>>({ map: 'B2' });
const [rects, setRects] = useState<Record<string, ExpandedRect>>({});
const rect = rects.map;
return (
<>
<Mono size="sm" secondary>
map: {rect ? `${rect.x},${rect.y} ${rect.w}×${rect.h}` : 'docked'}
</Mono>
<WindowManager
windows={WINDOWS}
assignments={assignments}
onAssignmentsChange={setAssignments}
expandedRects={rects}
onExpandedRectsChange={setRects}
/>
</>
);
}
Floating windows
Windows created at runtime, for example by an assistant, usually have no slot. Mark them
floating: they need no entry in assignments, are always expanded, can be moved and resized but
not docked, and there is no limit to how many are open. Without defaultRect they open centred and
each further one is offset by 24px. The window used last is drawn on top.
Floating windows are closable by default: the close button and Escape (while focus is inside the
window) call onClose with the id. Remove the window from windows to close it; WindowManager then
forgets its position. Set closable: true to give a docked window a close button as well.
import { useState } from 'react';
import { Button, Metric, WindowManager } from 'jarvis-react-ui';
import type { ManagedWindow } from 'jarvis-react-ui';
export default function Floating() {
const [windows, setWindows] = useState<ManagedWindow[]>([]);
const [focusedId, setFocusedId] = useState<string | null>(null);
const [count, setCount] = useState(0);
const open = () => {
const id = `scan-${count + 1}`;
setCount(count + 1);
setWindows([
...windows,
{
id,
title: `Scan ${count + 1}`,
badge: 'NEW',
floating: true,
defaultRect: { x: 40 + (count % 6) * 32, y: 80 + (count % 6) * 24, w: 260, h: 150 },
itemRenderer: () => <Metric value={(count + 1) * 17} unit="%" />,
},
]);
};
return (
<>
<div className="absolute top-3 left-3 z-[1]">
<Button variant="primary" onClick={open}>
Open window
</Button>
</div>
<WindowManager
windows={windows}
assignments={{}}
onAssignmentsChange={() => undefined}
focusedId={focusedId}
onFocusChange={setFocusedId}
onClose={(id) => setWindows(windows.filter((w) => w.id !== id))}
/>
</>
);
}
Windows
Each entry of windows is a ManagedWindow:
| Field | Type | Description |
|---|---|---|
id |
string |
Unique id; the key in assignments, modes and focusedId. |
title |
ReactNode |
Header title; also the accessible name of the window region. |
ix |
ReactNode |
Index glyph before the title. |
badge |
ReactNode |
Tag at the right end of the header. |
itemRenderer |
(props: PanelContentRenderProps) => ReactNode |
Renders the body; receives mode, focused and dragging. |
floating |
boolean |
Opens free-floating without a slot (see above). |
defaultRect |
ExpandedRect |
Initial position and size of a floating window. |
closable |
boolean |
Close button; default true for floating windows. |
A docked window without an entry in assignments is not rendered.
Sound
Through the nearest SfxProvider, dragging plays drag_start, a looping drag_move and drag_end;
resizing loops resize; the undock and dock buttons play click plus expand or collapse, and a
header double-click plays expand or collapse; the reset button plays click plus recall. A new
floating window plays menu_open; closing a window plays click plus menu_close (Escape plays
menu_close). See Sound effects.
Accessibility
- Each window is a non-modal
role="region"named by itstitlethrougharia-labelledby. - The reset and dock buttons are native buttons with accessible names; the dock button is the keyboard equivalent of the header double-click.
- A closable floating window is focusable (
tabIndex={-1}); Escape closes it while focus is inside. Controls inside the window that use Escape themselves (e.g. a dropdown) should callpreventDefault()so the window stays open. - Dragging, swapping and resizing are pointer-only; there are no keyboard shortcuts for them. Offer
another way to rearrange content (e.g. by updating
assignments) when keyboard users need it.
API
| Prop | Type | Default | Description |
|---|---|---|---|
assignments (required) |
Record<string, SlotId> |
Window id → slot id (controlled). Each window is docked in its assigned slot. | |
onAssignmentsChange (required) |
(next: Record<string, SlotId>) => void |
Called with the next assignments when a window is dropped on another slot (move), on an occupied slot (swap) or reset to its home slot. | |
windows (required) |
ManagedWindow[] |
Windows to render. A docked window without an entry in assignments is not rendered; floating windows need no assignment. |
|
className |
string |
Additional class names for the root element. | |
expandedRects |
Record<string, ExpandedRect> |
Window id → free-floating rect (controlled). When set, WindowManager no longer stores positions itself: apply the maps passed to onExpandedRectsChange to move and resize expanded windows. Omit it to let WindowManager track them. |
|
focusedId |
string | null |
null |
Id of the focused window (controlled); null for none. |
homeAssignments |
Record<string, SlotId> |
Home slots restored by a window's Reset button. Changes to this prop are picked up by the next reset. Without it, the assignments of the first render are used. |
|
modes |
Record<string, PanelMode> |
Window id → compact | expanded (controlled). Missing ids are compact. Omit it to let WindowManager track modes internally. |
|
onClose |
((id: string) => void) |
Called with the window id when its close button is clicked (or Escape is pressed in a focused floating window). Remove the window from windows to close it. |
|
onExpandedRectsChange |
((next: Record<string, ExpandedRect>) => void) |
Called with the full next rect map when a window is undocked, an expanded window is moved (on drag end) or resized (on every resize move), or a window is reset. Fires in both controlled and uncontrolled mode. | |
onFocusChange |
((id: string | null) => void) |
Called with a window id when a window is pressed, dragged or toggled, and with null on a pointer down outside every window. |
|
onModesChange |
((next: Record<string, PanelMode>) => void) |
Called with the full next mode map when a window is undocked, docked or reset. Fires in both controlled and uncontrolled mode. |
Type SlotId
Fixed 3×3 Dock grid slot geometry.
Left column (top → bottom): L1, L2, L3 Right column (top → bottom): R1, R2, R3 Bottom strip (left → right): B1, B2, B3
type SlotId = 'L1' | 'L2' | 'L3' | 'R1' | 'R2' | 'R3' | 'B1' | 'B2' | 'B3';
Type ManagedWindow
One window rendered by WindowManager.
| Field | Type | Default | Description |
|---|---|---|---|
id (required) |
string |
Unique window id; used as the key in assignments, modes and focusedId. |
|
itemRenderer (required) |
(props: PanelContentRenderProps) => ReactNode |
Render-prop content. Receives the current mode, focused state, and dragging state so the consumer can render compact vs expanded views. | |
badge |
ReactNode |
Tag shown at the right end of the header, e.g. LIVE. |
|
closable |
boolean |
true for floating windows, false for docked ones |
Shows a close button in the header that calls onClose; floating windows also close with Escape while focused. |
defaultRect |
ExpandedRect |
Initial position and size of a floating window, relative to the container. Without it, floating windows open centred and cascade by 24px. Ignored once the window has a rect in expandedRects. |
|
defaultSize |
{ w: number; h: number } |
{ w: 480, h: 320 } |
Initial size of a floating window without defaultRect; it still opens centred and cascades. |
floating |
boolean |
false |
Opens the window free-floating instead of docked: it needs no entry in assignments, is always expanded, can be moved and resized but not docked, and has no slot limit. Use it for windows created at runtime. |
ix |
ReactNode |
Index glyph shown before the title, e.g. ◈. |
|
title |
ReactNode |
Header title. It also names the window region (aria-labelledby). |
Type ExpandedRect
Rectangular geometry used for the expanded (free-floating) position.
| Field | Type | Default | Description |
|---|---|---|---|
h (required) |
number |
Height in px. | |
w (required) |
number |
Width in px. | |
x (required) |
number |
Left edge in px. | |
y (required) |
number |
Top edge in px. |
Type PanelMode
compact: docked in a slot. expanded: free-floating and resizable.
type PanelMode = 'compact' | 'expanded';
Type PanelContentRenderProps
Window state passed to itemRenderer, so content can adapt to docked or floating mode.
| Field | Type | Default | Description |
|---|---|---|---|
dragging (required) |
boolean |
Whether the window is being dragged or resized right now. | |
focused (required) |
boolean |
Whether the window is currently focused. | |
mode (required) |
PanelMode |
compact while docked in a slot, expanded while free-floating. |