Window

Window is the draggable, resizable panel shell that WindowManager places on its slot grid. Use it directly when you build your own window layout; for the standard nine-slot HUD, use WindowManager.

import { Window } from 'jarvis-react-ui';

Basic

A Window is absolutely positioned at position (x, y, w, h in pixels) inside its nearest positioned ancestor. The header shows ix, title and badge, and itemRenderer renders the body.

import { Window } from 'jarvis-react-ui';

export default function Basic() {
    return (
        <div className="relative h-[200px] w-[320px]">
            <Window
                id="system"
                title="System"
                ix="◈"
                badge="LIVE"
                position={{ x: 0, y: 0, w: 320, h: 200 }}
                draggable={false}
                itemRenderer={() => 'CPU 42% · RAM 61% · NET 12 MB/s'}
            />
        </div>
    );
}

Compact and expanded

Every window has a mode:

  • compact is the docked state. Resize handles are hidden, and the header shows an Undock button when you pass onModeToggle.
  • expanded is the free-floating state. The window gets an accent hairline and glow, the eight edge and corner handles become active (when resizable), and the header shows a Dock button.

Window does not switch modes itself. Clicking the dock/undock button or double-clicking the header calls onModeToggle, and you set the new mode. itemRenderer receives { mode, focused, dragging }, so the body can show a compact summary while docked and more detail while expanded.

WindowManager uses the modes like this: a compact window lives in a slot, and dragging its header moves it to another slot or swaps it with the window there, with SnapOverlay and SwapOverlay as feedback. An expanded window floats above the grid at a larger size, can be dragged anywhere and resized, and the reset button docks it back into its home slot.

Dragging and resizing

The window never moves itself: position is controlled. onDragMove and onResizeMove report the pointer offset from the start of the gesture (dx, dy), and onResizeMove also reports the handle direction (n, se, ...). Remember the rect when the gesture starts and apply the offset to it. onDragStart and onResizeStart receive the native pointerdown PointerEvent, like the move and end callbacks. Drag the header below, undock the window, then drag its edges.

import { useRef, useState } from 'react';
import { Window } from 'jarvis-react-ui';
import type { PanelMode, ResizeDir } from 'jarvis-react-ui';

type Rect = { x: number; y: number; w: number; h: number };

const START: Rect = { x: 16, y: 16, w: 300, h: 150 };

function resize(r: Rect, dir: ResizeDir, dx: number, dy: number): Rect {
    const next = { ...r };
    if (dir.includes('e')) next.w = Math.max(200, r.w + dx);
    if (dir.includes('s')) next.h = Math.max(100, r.h + dy);
    if (dir.includes('w')) next.w = Math.max(200, r.w - dx);
    if (dir.includes('n')) next.h = Math.max(100, r.h - dy);
    if (dir.includes('w')) next.x = r.x + r.w - next.w;
    if (dir.includes('n')) next.y = r.y + r.h - next.h;
    return next;
}

export default function DragAndResize() {
    const [rect, setRect] = useState<Rect>(START);
    const [mode, setMode] = useState<PanelMode>('compact');
    const origin = useRef(rect);

    return (
        <div className="relative h-[340px] w-full">
            <Window
                id="notes"
                title="Notes"
                ix="▸"
                position={rect}
                mode={mode}
                onDragStart={() => {
                    origin.current = rect;
                }}
                onDragMove={(_id, dx, dy) =>
                    setRect({
                        ...origin.current,
                        x: origin.current.x + dx,
                        y: origin.current.y + dy,
                    })
                }
                onResizeStart={() => {
                    origin.current = rect;
                }}
                onResizeMove={(_id, dir, dx, dy) => setRect(resize(origin.current, dir, dx, dy))}
                onModeToggle={() => setMode((m) => (m === 'compact' ? 'expanded' : 'compact'))}
                onReset={() => {
                    setRect(START);
                    setMode('compact');
                }}
                itemRenderer={({ mode: m }) =>
                    m === 'compact'
                        ? 'Drag the header. Undock to resize.'
                        : 'Expanded: drag the edges and corners to resize.'
                }
            />
        </div>
    );
}

During a gesture the root element gets data-state="dragging" or "resizing". The other values of state (snap-preview, swap-preview, settling, focused) are styling hooks for the parent: settling animates position and size changes, and focused raises the window above its siblings. Position and size reach the stylesheet as the CSS variables --lib-window-x, --lib-window-y, --lib-window-w and --lib-window-h on the root element. To build drag-and-drop between slots, combine Window with useSlotDrag and the slot grid helpers.

Header actions

The reset, dock/undock and close buttons are only rendered when you pass onReset, onModeToggle or onClose. Pressing them never starts a drag. stackIndex raises an expanded window above others with a lower index; WindowManager sets it for you.

import { useState } from 'react';
import { Button, Window } from 'jarvis-react-ui';

export default function HeaderActions() {
    const [open, setOpen] = useState(true);
    const [resets, setResets] = useState(0);

    if (!open) return <Button onClick={() => setOpen(true)}>Reopen window</Button>;

    return (
        <div className="relative h-[180px] w-[320px]">
            <Window
                id="agenda"
                title="Agenda"
                ix="▦"
                position={{ x: 0, y: 0, w: 320, h: 180 }}
                draggable={false}
                onReset={() => setResets((n) => n + 1)}
                onClose={() => setOpen(false)}
                itemRenderer={() => `09:30 · Standup — reset ${resets}×`}
            />
        </div>
    );
}

Sound

Through the nearest SfxProvider:

  • Dragging the header plays drag_start, loops drag_move and plays drag_end.
  • Resizing loops resize until the pointer is released.
  • The reset button plays click and recall; the dock/undock button plays click plus expand (undock) or collapse (dock); the close button plays click and menu_close.
  • Double-clicking the header plays the same expand or collapse sound as the dock/undock button (without the click).
  • Hovering the window plays hover_panel; hovering a header button plays hover_button.

See Sound effects.

Accessibility

  • The root is a non-modal role="region" landmark. aria-labelledby points at the title element, so any title (string or element) names it; without a title the region is unnamed.
  • Window does not move or trap focus; Tab moves through its content in document order like any other part of the page.
  • The header buttons are native buttons named Reset window, Undock window / Dock window and Close window, so they work with the keyboard. The dock/undock button is the keyboard equivalent of the header double-click.
  • With closeOnEscape and onClose, the window is focusable (tabIndex={-1}) and Escape calls onClose while focus is inside it.
  • Dragging and resizing are pointer-only: there are no arrow-key or other keyboard shortcuts to move or resize a window. Offer another way to move or resize content when keyboard users need it.

API

Prop Type Default Description
id (required) string Unique window id; passed back as the first argument of every callback.
itemRenderer (required) (props: PanelContentRenderProps) => ReactNode Renders the window body; receives the current mode, focused and dragging.
position (required) { x: number; y: number; w: number; h: number; } Position and size in pixels, relative to the nearest positioned ancestor. The window does not move itself: apply the deltas from onDragMove / onResizeMove to this rect.
badge ReactNode Tag shown at the right end of the header.
className string Extra classes for the root <div role="region"> element.
closeOnEscape boolean false Calls onClose when Escape is pressed while focus is inside the window.
draggable boolean true Whether the header can be dragged.
focused boolean false Whether the window is focused; highlights the panel chrome.
ix ReactNode Index glyph shown before the title in the header.
mode 'compact' | 'expanded' 'compact' compact hides the resize handles and shows the undock button, expanded shows the handles and the dock button.
onClose ((id: string) => void) Shows a close button in the header and is called when it is clicked. Closing plays the menu_close sound.
onDragEnd ((id: string, e: PointerEvent) => void) Called when the header drag ends (pointer up or cancel).
onDragMove ((id: string, dx: number, dy: number, e: PointerEvent) => void) Called on every pointer move of a header drag with the offset from the drag start.
onDragStart ((id: string, e: PointerEvent) => void) Called with the native pointerdown event when a header drag starts (not on the header buttons).
onFocus ((id: string) => void) Called with the id on any pointer down inside the window.
onModeToggle ((id: string) => void) Shows a dock/undock button in the header; called when it is clicked or the header is double-clicked. Both play the expand / collapse sound.
onReset ((id: string) => void) Shows a reset button in the header and is called when it is clicked.
onResizeEnd ((id: string, e: PointerEvent) => void) Called when the resize ends.
onResizeMove ((id: string, dir: ResizeDir, dx: number, dy: number, e: PointerEvent) => void) Called on every pointer move of a resize with the handle direction and offset.
onResizeStart ((id: string, dir: ResizeDir, e: PointerEvent) => void) Called with the native pointerdown event when a resize starts on one of the eight edge and corner handles.
resizable boolean true Whether the resize handles are rendered; they are visible in expanded mode only.
stackIndex number 0 Stacking rank among expanded windows; a higher rank is drawn on top. Set by WindowManager to bring the last used floating window to the front.
state 'idle' | 'dragging' | 'snap-preview' | 'swap-preview' | 'settling' | 'focused' | 'resizing' 'idle' Visual state; dragging and resizing are also set automatically during a gesture.
title ReactNode Header title. It also names the window region (aria-labelledby points at it).

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.

Type ResizeDir

Edge or corner a window is resized from, as compass directions (se = bottom-right corner).

type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';

Type PanelMode

compact: docked in a slot. expanded: free-floating and resizable.

type PanelMode = 'compact' | 'expanded';