nexus-shell
v0.2.11
Published
React component library for building IDE-style application shells: menu bar, activity bar, sidebar, docking layout, terminal, chat and status bar.
Maintainers
Readme
Nexus Shell
React components for building IDE-style application shells — menu bar, activity bar, sidebar, docking layout, terminal, chat pane and status bar.
Documentation and live examples → · Storybook
npm install nexus-shellimport 'nexus-shell/style.css';React 19 is a peer dependency.
Start with the shell
An application built on this library starts with ShellLayout and grows by
registering features into it.
// main.tsx — a complete application
import { createRoot } from 'react-dom/client';
import { Boxes, Files } from 'lucide-react';
import {
AppTitle,
ShellLayout,
chatPanel,
componentRegistry,
initializeShell,
terminalPanel,
useLayoutStore,
} from 'nexus-shell';
import 'nexus-shell/style.css';
// Views register by id, so the shell never has to import them.
componentRegistry.register('editor', Editor);
initializeShell({
panels: [
{ id: 'files', label: 'Explorer', icon: Files, component: FileExplorer },
terminalPanel(),
],
// Chat on the right rail. Move it to `panels` for the left, or register
// the component as a tab — nothing about it is bound to one edge.
inspectorPanels: [chatPanel()],
commands: [
{ id: 'file.save', label: 'File: Save', keybinding: 'Control+s', execute: save },
],
menus: { File: [{ id: 'save', label: 'Save', commandId: 'file.save' }] },
statusBar: [{ id: 'branch', label: 'main', alignment: 'left' }],
});
useLayoutStore.getState().addTab('editor', 'App.tsx');
createRoot(document.getElementById('root')!).render(
<ShellLayout title={<AppTitle title="Acme Studio" icon={<Boxes size={16} />} />} />,
);That is a working IDE: dockable tabs, a command palette on Cmd/Ctrl+Shift+P, a
terminal, a chat pane, and a themable frame.
Where things go
The shell has four places a view can live, and nothing is nailed to one of them. Chat and the terminal are ordinary registrations, so they go wherever you put them — including into a dockable tab, alongside your own views.
| Place | How |
| :-- | :-- |
| Left rail | panels: [...] |
| Right rail | inspectorPanels: [...] |
| Dockable tab | componentRegistry.register(id, Component) + addTab(id) |
| Bottom of the workspace | A tab in the bottom row of your initialLayoutJson |
Anything hosted in a pane or a tab is told so, and drops its own title bar —
SidebarPane and the tab strip already draw one. Pass chrome explicitly to
force it either way.
Growing an app
You add capability by registering it. None of this changes the layout, which is what lets plugins and distant code contribute to the shell.
| To add | Do this |
| :-- | :-- |
| A view or tab | componentRegistry.register(id, Component), then useLayoutStore().addTab(id, title) |
| An action or hotkey | commandRegistry.registerCommand({ id, label, keybinding, execute }) — appears in the palette automatically |
| A menu entry | menuRegistry.registerMenu('File', { id, label, commandId }) |
| A sidebar panel | Add it to panels; it becomes an activity bar icon |
| An inspector or properties panel | Add it to inspectorPanels; it docks right, with its own rail |
| Chat or a terminal, anywhere | chatPanel() / terminalPanel() in panels or inspectorPanels, or register the component as a tab |
| A status bar item | useStatusBarStore.getState().addWidget(...) |
| A dialog from anywhere | await useModalStore.getState().openConfirm('Sure?') |
| An unsaved-changes guard | useLayoutStore.getState().setTabDirty(tabId, true) |
When you need less than the shell
Every component the shell is assembled from is exported on its own — pure and
prop-driven, with no global state or registry lookups. This is the escape hatch:
reach for it when you're embedding one piece into an app you already have, or
building a frame ShellLayout can't express.
import { ActivityBar, SidebarPane, StatusBar, TreeWidget } from 'nexus-shell';
const [active, setActive] = useState<string | null>('files');
<div className="theme-dark flex h-screen flex-col">
<div className="flex flex-1">
<ActivityBar
items={panels}
activeId={active}
onSelect={(id) => setActive(id === active ? null : id)}
/>
{active && (
<SidebarPane title="Explorer" onClose={() => setActive(null)}>
<TreeWidget data={nodes} onToggle={toggle} onActivate={open} />
</SidebarPane>
)}
<main className="flex-1">{children}</main>
</div>
<StatusBar widgets={[{ id: 'branch', label: 'main', alignment: 'left' }]} />
</div>;These are controlled, so state stays in your app — a real cost the shell was absorbing for you.
Between the two sit the Connected* variants, each binding one component to its
store. ShellLayout is built entirely out of them, so you can rearrange the
shell's own frame and keep every registration working.
Components
ShellLayout assembles all of these. Each is also exported on its own.
| Component | What it is |
| :-- | :-- |
| ActivityBar | Vertical icon rail for switching side panels, either edge |
| ChatPane | Chat transcript with slash-command autocomplete; fills any host |
| CommandPalette | Filterable, keyboard-navigable command list |
| ContextMenu | Floating menu positioned at viewport coordinates |
| DataGrid | Sortable, filterable table with optional virtualisation |
| MenuBar | Application menu bar with submenus and content slots |
| Modal | Alert / confirm / prompt dialog |
| QuickSearch | Compact search field with grouped results, for the menu bar |
| SearchWidget | Full-height search panel with autocomplete, for the sidebar |
| SettingsPanel | Theme picker body for the settings sidebar |
| SidebarPane | Titled, closable side panel; side docks it left or right |
| StatusBar | Footer with left / center / right item groups |
| TerminalPane | Terminal log and input; fills any host |
| PaneHostProvider | Tells hosted components a title bar is already drawn |
| ThemeSwitcher | Segmented theme control |
| TreeWidget | Virtualised tree for any hierarchy, with drag-to-move and a data-driven menu |
| GraphCanvas | Infinite pannable, zoomable field for node-and-edge editing |
| GraphNode | Positioned, focusable node with edge ports |
| GraphEdge | Directed edge with bezier / smoothstep / straight routing |
| GraphMiniMap | Scaled graph overview with a viewport indicator |
| NodePalette | Drag-to-create and keyboard-to-create node types |
| useGraphKeyboard | Spatial keyboard navigation and editing for a graph |
| useGraphLayout | Auto layout engines, with a freeform escape for hand placement |
| layeredLayout | Layered tree layout in four directions; handles cycles |
| gridLayout | Uniform grid, ignoring edges |
| PropertyPanel | Inspector for the current selection, with mixed-value editing |
| TextField … StaticField | Nine composable property fields, usable on their own |
| FieldShell | The label / description / error frame, for fields of your own |
| UserProfile | Avatar and identity widget |
Theming
Four themes ship in the stylesheet: theme-light, theme-dark, theme-gt
(Georgia Tech) and theme-tamu (Texas A&M).
Colors are CSS custom properties scoped to those classes, so switching themes is
a class on an ancestor — no React context, no provider.
Add your own by defining a .theme-yourname block with the same properties.
Development
npm install
npm run site # documentation site at :5173
npm run storybook # component workbench at :6006
npm run dev # showcase app at :5173
npm run typecheck # library sources
npm test # vitest — unit and architecture tests
npm run site:test # renders every site route and demo
npm run build # library build to dist/
npm run build-storybook
npm run site:build # documentation site to website/dist/
npm run e2e # Playwright against the showcase appnpm run typecheck covers src/ only. To include the examples:
npx tsc --noEmit -p tsconfig.examples.jsonCI runs all of the above on every push and pull request.
Layout
src/
components/ Pure, prop-driven components — what the shell is made of
connected/ Store-bound wrappers used by ShellLayout
core/ Stores (zustand), registries, and initializeShell
lib/ Shared utilities
stories/ MDX documentation pages
examples/
showcase/ Demo apps built on the library — not published
dialogue-mapper/
mockup-reviewer/
wiki/
wargame/
e2e/ Playwright specs for the showcase app
basic-app/ Minimal integration example
website/
src/demos/ Live examples — the source shown on the site
src/content/ The component gallery, as data
scripts/ Generates the props tables from the library's typesDocumentation site
website/ is a Vite app that imports the library from ../src, not from
dist. Every example on the site is rendered by the current source, so a demo
that no longer compiles against the real API fails the build rather than
quietly documenting an API that no longer exists.
Two generation steps keep it honest:
- Props tables come from
website/scripts/generate-api.mjs, which parses the component sources for prop names, types, defaults and JSDoc. Nothing is hand-written. - Code samples are extracted from the demo files by
#regionmarker, so the snippet a visitor copies is provably the code that produced the thing they just interacted with.
Adding a component to the site is one entry in website/src/content/components.ts
plus a demo file. There is no per-component page to keep in sync.
The site ships no dependencies of its own — routing, syntax highlighting and the copy button are all local, so the docs can't drag the repo's install surface around.
Deployment. .github/workflows/deploy-pages.yml builds the site and
Storybook on every push to main and publishes them as one Pages artifact — the
site at the root, Storybook under /storybook/. First-time setup needs your
credentials: run ./scripts/setup-pages.sh.
The split between components/ and connected/ is the load-bearing one. A
component that reads a global store can't be configured by its caller, rendered
twice with different data, or documented — its props table is empty because it
has no props. Everything in components/ is therefore forbidden from importing
core/services, and the tests in components/__tests__ exist to keep it that
way.
Anything under examples/ is a consumer of the library, not part of it. It's
built and documented alongside the components to prove the primitives are
sufficient, but it ships to nobody.
License
MIT
