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 its title through aria-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 call preventDefault() 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.