Panels · Guides
Apps
Declare every panel once with defineApp; the launcher, taskbar and search follow.
An app is a panel you can open: its id, title, icon, where it starts and what it renders. Declare each one once, pass the list to WindowManager, and everything that lists or opens panels reads from it.
import { defineApp } from "@nynxz/react-panels";
export const profile = defineApp({
id: "profile",
title: "profile",
icon: <UserIcon />,
description: "Who I am", // shown in search
keywords: ["about", "contact"], // also matched by search
tile: { side: "left" }, // open tiled, on the left
defaultRect: { x: 40, y: 40, w: 420, h: 440 }, // when floating
minSize: { w: 320, h: 240 },
render: () => <Profile />,
});
Where an app opens
Without tile, an app opens floating at defaultRect (or cascaded from the last window). With tile, it joins the tiling:
tile | Opens |
|---|---|
{ side: "left" } | on that side of the whole layout |
{ of: "profile", side: "right" } | splitting profile's tile, on its right |
If the app named in of isn't tiled when this one opens, it falls back to that side of the whole layout, so the order apps open in never breaks the result.
A new split is even by default. share sets the app's part of it instead, so a sidebar or a short list comes back at its size however often it's closed and reopened:
defineApp({ id: "contents", tile: { side: "left", share: 0.2 } /* … */ });
defineApp({
id: "advisories",
tile: { of: "profile", side: "bottom", share: 0.25 } /* … */,
});
Starting layout
workspaceWithApps builds the state you'd get by launching apps in order, which makes a good defaultState. The last app opened ends up focused; focus picks another:
const initial = workspaceWithApps(apps, ["profile", "work", "advisories"], {
bounds: { x: 0, y: 0, w: 1280, h: 764 }, // plausible size, for pre-rendering
focus: "profile",
});
Pass realistic bounds when rendering on the server: tiled windows are positioned in CSS proportions, so they're right at any size on first paint, but floating ones use pixels.
Opening apps
useApps() gives the list and launch(id), which opens the app (tiling it if it says where) or, if it's already open, raises it, and moves keyboard focus into the window either way.
const { apps, launch } = useApps();
return apps.map((app) => (
<button key={app.id} onClick={() => launch(app.id)}>
{app.icon} {app.title}
</button>
));
When a window opens by itself to show a result, rather than because someone asked for it, pass { focus: false } so keyboard focus stays where the visitor is working, such as a search box:
useEffect(() => {
if (selected) launch("finding", { focus: false });
}, [selected, launch]);
Switching features off
Every app can turn window features off with features; see Window features.
On phones
A phone has no room for a desktop, so a layout can stack the apps as plain panels instead (the site's StackedApps does this). Every app is included unless it opts out with stack: false, which suits apps that only make sense as windows:
defineApp({ id: "keys", title: "keys.txt", stack: false /* … */ });