Imported from Anupheaus/react-ui (
src/components/Windows/AGENTS.md). Install upstream withnpx skills add Anupheaus/react-ui --skill Windows. Copyright stays with the author.
Windows Component
Overview
The Windows component renders draggable, resizable application windows. Window definitions are registered globally at createWindow time—they do not need to be passed as children.
createWindow behaves like createContext: call it once at module scope to define a window type, then open instances from anywhere with useWindow. Never render a createWindow component in JSX (e.g. <MyWindow />) to "host" or "register" it — it is already registered globally, and every open instance is rendered by the mounted <Windows /> host, not where you place the element. Rendering it yourself mounts a second renderer and makes the window appear on mount.
Because window content is rendered at the <Windows /> host — outside the React tree of whatever opened it — it does not inherit React context or props from the opener. Pass everything the content needs as window args (see Passing data to window content). A context provider wrapped around the opener will not reach the window, and would not survive persistence either (a persisted window re-materialises on reload with no surrounding providers).
Usage
1. Mount the Windows component
<Windows />
Or with optional persistence and callbacks:
<Windows
localStorageKey="my-app-windows"
onChange={(states) => console.log('Windows changed', states)}
/>
2. Define windows with createWindow
const MyWindow = createWindow('MyWindow', ({ Window, Content, id }) => (arg1: string, arg2?: number) => (
<Window title={`Window ${id}`}>
<Content>
<p>{arg1} - {arg2}</p>
</Content>
</Window>
));
3. Open windows with useWindow
When you provide an id at hook level (useWindow(definition, id)):
open(args)— id is fixed; only pass window argsclose(response?)— closes the window identified by the hook id
const { openMyWindow, closeMyWindow } = useWindow(MyWindow, 'my-window-id');
await openMyWindow('hello', 42);
await closeMyWindow('done');
When you do not provide an id (useWindow(definition)):
open(id, args)— id is required as the first argumentclose(id, response?)— id required to target which window to close
const { openMyWindow, closeMyWindow } = useWindow(MyWindow);
await openMyWindow('instance-1', 'hello', 42);
await openMyWindow('instance-2', 'world');
await closeMyWindow('instance-1', 'done');
When you provide no arguments (useWindow()):
- Returns utilities for the current window — must be called from within window content (inside a window created with
createWindow). setTitle(title)— update the window title dynamicallyclose(response?)— close the current window
const MyWindow = createWindow('MyWindow', ({ Window, Content, id }) => (name: string) => (
<Window title={`Hello ${name}`}>
<Content>
<MyContent />
</Content>
</Window>
));
function MyContent() {
const { setTitle, close } = useWindow();
return (
<>
<button onClick={() => setTitle('Updated title')}>Change title</button>
<button onClick={() => close('saved')}>Save & close</button>
</>
);
}
4. Passing data to window content
Window content gets its data only from the args passed at open time — never from React context/props of the opener, which are out of reach (see Overview). The args are the parameters of the definition's inner function, and open is fully typed from them:
interface EditUserValue {
user: User;
save(user: User): Promise<void>; // a callback is fine — see persistence below
}
const EditUserWindow = createWindow('EditUserWindow', ({ Window, Content }) => (value: EditUserValue) => (
<Window title={`Edit ${value.user.name}`}>
<Content>{/* edit value.user, call value.save(...) */}</Content>
</Window>
), { doNotPersist: true }); // args hold a function + class instance → not serialisable → do not persist
// opener — no provider, no inline <EditUserWindow />, just open with the value:
const { openEditUserWindow } = useWindow(EditUserWindow);
await openEditUserWindow(user.id, { user, save });
Args are a snapshot taken at open time; the window will not re-read the opener's state as it changes. Seed local state from the args and drive the rest from within the window (its own hooks, the save callback, useWindow() to close).
If any arg is non-serialisable — a function (e.g. a
savecallback), a class instance, a luxonDateTime, etc. — you MUST setdoNotPersist: true. Otherwise, with persistence enabled, the window is either silently dropped from storage or re-materialises on reload with broken args (a dead function reference, a plain object where a class instance was expected). A window is safe to persist only when every arg is plain JSON.
Props
| Prop | Type | Description |
|---|---|---|
id |
string |
Manager id. Defaults to windows-default. |
className |
string |
CSS class for the container. |
children |
ReactNode |
Optional content rendered alongside windows. |
states |
WindowState[] |
Controlled initial/restored states. |
localStorageKey |
string |
When provided, persists window state to localStorage. Omit to disable persistence. |
onChange |
(states) => void |
Called when window states change. |
Persistence
Persistence is disabled by default. Pass localStorageKey to enable:
<Windows localStorageKey="my-windows" />
Only windows with simple (JSON-serializable) args are persisted. Any window whose args include non-serialisable values (functions, class instances, luxon DateTime, etc.) must set doNotPersist: true in its createWindow options — see Passing data to window content. A persisted window re-materialises on reload from its stored args alone, with no surrounding providers or live callbacks, so non-serialisable args cannot survive the round trip.
Notes
- The
Windowscomponent must be mounted before anyopencalls. Manager lookup is deferred until open/close/focus/etc. are invoked, so opening inuseLayoutEffectworks even when the component usinguseWindowis an ancestor ofWindows. InternalWindowsis used internally byDialogsand is not part of the public API.
Contexts and content-based sizing
- WindowRenderContext is provided by
WindowRendererand holdsid,managerId,close,setTitle,title. It is consumed byWindow,useWindow()(no-arg form), andWindowAction. Definitions do not receive or pass any context toWindow. - WindowContext is provided by
Windowand holds only{ disableScrolling?: boolean }. It is consumed byWindowContentso content can opt out of the default Scroller (e.g. for layout that defines intrinsic height).
Window sizing:
- disableScrolling can be set on
Window. Whentrue, it is passed viaWindowContexttoWindowContent(which then does not wrap children inScroller), and the window's initial height can be driven by the measured content wrapper. WhendisableScrollingisfalse, height follows the existing ResizeObserver/minHeight path. - Width is set from the inner content wrapper measurement when no explicit width is provided (during the preparation phase).
- Height from the content wrapper is used only when
WindowhasdisableScrollingand no explicit height; otherwise height comes from ResizeObserver/minHeight. - Auto-size (from content or ResizeObserver) runs only while the window is in the preparation phase (
preparationClassName !== undefined). After preparation completes, dimensions change only via user resize.
Decision rationale
Why registerGlobal exists
createWindow is called at module load time, before any React component has mounted. The window definition must be available globally so that useWindow can open a window from any component in the tree, including components that are ancestors of the <Windows /> mount point. If definitions were passed as children or props, they could only be consumed by components that are descendants of <Windows />, which would require <Windows /> to sit at the top of every app tree and prevent code-splitting window definitions.
registerGlobal stores the definition in the module-level windowsDefinitionsManager singleton. When <Windows /> mounts it calls registerManager, which pairs each global definition with the concrete manager that will render it. Opening is deferred until a manager is available, so useWindow can be called from a component that renders before <Windows />.
Why there are two separate contexts — WindowContext vs WindowRenderContext
WindowRenderContext is provided by WindowRenderer and carries the runtime wiring for a single open window instance: its id, managerId, the close callback, and setTitle. It is consumed by Window, useWindow() (no-arg form), and WindowAction. This context exists for the lifetime of the rendered window.
WindowContext is provided by Window itself and carries only { disableScrolling }. It exists solely so that WindowContent — a separate component one level down — can opt out of the default Scroller wrapper without needing a prop drilled through. The narrow scope is intentional: if WindowContext carried the full render wiring it would conflict when windows are nested.
Keeping the two contexts separate means adding new per-window-instance data to WindowRenderContext never risks leaking into WindowContent's scroll decision, and vice versa.
Ambiguities and gotchas
Persistence requires localStorageKey — omitting it silently disables all persistence
Window positions, sizes, and open/closed states are only persisted when <Windows localStorageKey="..." /> is provided. Without it, every page reload resets all windows. The prop is optional with no default, so forgetting it produces no warning.
Window position resets on remount without persistence
If <Windows /> unmounts and remounts (e.g. during a route transition), all open window state is lost unless localStorageKey is provided. There is no in-memory fallback between mounts; the state lives only in localStorage.
createWindow vs useWindow vs registerGlobal — when to use each
createWindow— call once at module scope (outside any component) to define a window type. It automatically callsregisterGlobal. Never call it inside a component or effect.useWindow(MyWindow)/useWindow(MyWindow, id)— call inside a component to get open/close/focus/restore/maximize commands for a specific window type. The hook form with no id returns commands that require an id at open time; the form with an id fixes the id at hook time.useWindow()(no args) — call from inside window content (a component rendered by acreateWindowdefinition) to getcloseandsetTitlefor the current window. Throws if called outside window content.registerGlobalis an implementation detail onwindowsDefinitionsManager— it is called bycreateWindowand should never be called directly.
<Windows /> must be mounted before any open call resolves
useWindow defers manager lookup until open is called, not at hook time. Calling open before <Windows /> has mounted will throw because no manager is registered. Opening in useLayoutEffect on a component that is a sibling or ancestor of <Windows /> is safe as long as <Windows /> has mounted first in the same layout pass.
Never render a createWindow component in JSX, and never feed a window via the opener's context
createWindow registers globally (like createContext), so a window opens purely through useWindow(...).openX(...). Rendering the component yourself — <MyWindow />, or a definitionId-keyed "host" element inside the opener — mounts an extra renderer that shows the window as soon as that element mounts (a window appearing unbidden when its parent opens is the tell). It is also tempting to wrap that inline element in a context provider to feed the window data; that only appears to work because the element sits in the opener's tree. The real open instance renders at the <Windows /> host, outside that tree, so the provider never reaches it — and a persisted window re-materialises with no providers at all. Pass data as args instead (see Passing data to window content).
doNotPersist: true in createWindow options excludes a window type from localStorage
Only windows whose args are JSON-serializable and whose definition does not set doNotPersist: true are written to localStorage. If a window type stores non-serializable args (e.g. functions, class instances, luxon DateTime), it is silently skipped during persistence — so set doNotPersist: true whenever args are non-serialisable. There is no warning.
Related
- Window/AGENTS.md — internal rendering layer:
Window.tsx,WindowContent,WindowAction/Actions/OkAction, the preparation-phase sizing hooks (useWindowDimensions,useWindowEvents,useWindowState), andWindowValidationContext - ../Dialog/AGENTS.md —
Dialogis built on top ofInternalWindowswithmanagerType="dialogs"; it sharescreateWindowdefinitions but usesuseDialograther thanuseWindow - ../../providers/UIStateProvider/AGENTS.md —
UIStateProviderwith astorageKeyis an alternative persistence mechanism used elsewhere in the library; the WindowslocalStorageKeyprop is its own direct localStorage integration independent of that provider