nexus-shell 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,17 +16,74 @@ import 'nexus-shell/style.css';
16
16
 
17
17
  React 19 is a peer dependency.
18
18
 
19
- ## Two tiers
19
+ ## Start with the shell
20
20
 
21
- **Primitives** are pure and prop-driven. No global state, no registry lookups.
22
- You pass data in, you get callbacks out.
21
+ An application built on this library starts with `ShellLayout` and grows by
22
+ registering features into it.
23
23
 
24
24
  ```tsx
25
- import { StatusBar, ActivityBar, SidebarPane, TreeWidget } from 'nexus-shell';
25
+ // main.tsx — a complete application
26
+ import { createRoot } from 'react-dom/client';
27
+ import { Boxes, Files } from 'lucide-react';
28
+ import {
29
+ AppTitle,
30
+ ShellLayout,
31
+ componentRegistry,
32
+ initializeShell,
33
+ useLayoutStore,
34
+ } from 'nexus-shell';
35
+ import 'nexus-shell/style.css';
36
+
37
+ // Views register by id, so the shell never has to import them.
38
+ componentRegistry.register('editor', Editor);
39
+
40
+ initializeShell({
41
+ panels: [{ id: 'files', label: 'Explorer', icon: Files, component: FileExplorer }],
42
+ commands: [
43
+ { id: 'file.save', label: 'File: Save', keybinding: 'Control+s', execute: save },
44
+ ],
45
+ menus: { File: [{ id: 'save', label: 'Save', commandId: 'file.save' }] },
46
+ statusBar: [{ id: 'branch', label: 'main', alignment: 'left' }],
47
+ });
48
+
49
+ useLayoutStore.getState().addTab('editor', 'App.tsx');
50
+
51
+ createRoot(document.getElementById('root')!).render(
52
+ <ShellLayout title={<AppTitle title="Acme Studio" icon={<Boxes size={16} />} />} />,
53
+ );
54
+ ```
55
+
56
+ That is a working IDE: dockable tabs, a command palette on `Cmd/Ctrl+Shift+P`, a
57
+ terminal, a chat pane, and a themable frame.
58
+
59
+ ## Growing an app
60
+
61
+ You add capability by **registering** it. None of this changes the layout, which
62
+ is what lets plugins and distant code contribute to the shell.
63
+
64
+ | To add | Do this |
65
+ | :-- | :-- |
66
+ | A view or tab | `componentRegistry.register(id, Component)`, then `useLayoutStore().addTab(id, title)` |
67
+ | An action or hotkey | `commandRegistry.registerCommand({ id, label, keybinding, execute })` — appears in the palette automatically |
68
+ | A menu entry | `menuRegistry.registerMenu('File', { id, label, commandId })` |
69
+ | A sidebar panel | Add it to `panels`; it becomes an activity bar icon |
70
+ | A status bar item | `useStatusBarStore.getState().addWidget(...)` |
71
+ | A dialog from anywhere | `await useModalStore.getState().openConfirm('Sure?')` |
72
+ | An unsaved-changes guard | `useLayoutStore.getState().setTabDirty(tabId, true)` |
73
+
74
+ ## When you need less than the shell
75
+
76
+ Every component the shell is assembled from is exported on its own — pure and
77
+ prop-driven, with no global state or registry lookups. This is the escape hatch:
78
+ reach for it when you're embedding one piece into an app you already have, or
79
+ building a frame `ShellLayout` can't express.
80
+
81
+ ```tsx
82
+ import { ActivityBar, SidebarPane, StatusBar, TreeWidget } from 'nexus-shell';
26
83
 
27
84
  const [active, setActive] = useState<string | null>('files');
28
85
 
29
- <div className="theme-light flex h-screen flex-col">
86
+ <div className="theme-dark flex h-screen flex-col">
30
87
  <div className="flex flex-1">
31
88
  <ActivityBar
32
89
  items={panels}
@@ -44,29 +101,18 @@ const [active, setActive] = useState<string | null>('files');
44
101
  </div>;
45
102
  ```
46
103
 
47
- **`ShellLayout`** is the assembled frame — the same components, wired to a set of
48
- zustand stores and registries so you configure declaratively:
49
-
50
- ```tsx
51
- import { ShellLayout, componentRegistry } from 'nexus-shell';
52
-
53
- componentRegistry.register('editor', Editor);
54
-
55
- <ShellLayout
56
- title={<Logo />}
57
- panels={[{ id: 'files', label: 'Explorer', icon: Files, component: FileTree }]}
58
- menuConfig={{ File: [{ id: 'save', label: 'Save', commandId: 'file.save' }] }}
59
- statusBarConfig={[{ id: 'branch', label: 'main', alignment: 'left' }]}
60
- />;
61
- ```
104
+ These are controlled, so state stays in your app — a real cost the shell was
105
+ absorbing for you.
62
106
 
63
- Each primitive also has a `Connected*` variant that binds it to the matching
64
- store. `ShellLayout` is built out of those, and they're exported, so you can mix
65
- tiers freely.
107
+ Between the two sit the `Connected*` variants, each binding one component to its
108
+ store. `ShellLayout` is built entirely out of them, so you can rearrange the
109
+ shell's own frame and keep every registration working.
66
110
 
67
111
  ## Components
68
112
 
69
- | Primitive | What it is |
113
+ `ShellLayout` assembles all of these. Each is also exported on its own.
114
+
115
+ | Component | What it is |
70
116
  | :-- | :-- |
71
117
  | `ActivityBar` | Vertical icon rail for switching sidebar panels |
72
118
  | `ChatPane` | Docked chat transcript with slash-command autocomplete |
@@ -85,8 +131,6 @@ tiers freely.
85
131
  | `TreeWidget` | Virtualised file tree with drag-to-move and a data-driven menu |
86
132
  | `UserProfile` | Avatar and identity widget |
87
133
 
88
- `ShellLayout` composes all of them.
89
-
90
134
  ## Theming
91
135
 
92
136
  Three themes ship in the stylesheet: `theme-light`, `theme-dark`, `theme-gt`.
@@ -125,9 +169,9 @@ CI runs all of the above on every push and pull request.
125
169
 
126
170
  ```
127
171
  src/
128
- components/ Primitives — pure, prop-driven, the public API
172
+ components/ Pure, prop-driven components — what the shell is made of
129
173
  connected/ Store-bound wrappers used by ShellLayout
130
- core/ Stores (zustand) and registries
174
+ core/ Stores (zustand), registries, and initializeShell
131
175
  lib/ Shared utilities
132
176
  stories/ MDX documentation pages
133
177
  examples/
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { Model } from 'flexlayout-react';
2
+ import { Model, IJsonModel } from 'flexlayout-react';
3
3
  import { IMenuItemConfig } from '../../core/registry/MenuRegistry';
4
4
  import { ISlashCommandConfig } from '../../core/services/ChatService';
5
5
  import { ISidebarPanel } from '../../core/services/SidebarService';
@@ -29,35 +29,67 @@ export interface ShellLayoutProps {
29
29
  * stories. Omit it to use the store's persisted model.
30
30
  */
31
31
  layoutModel?: Model;
32
+ /**
33
+ * Starting workspace, as a `flexlayout-react` JSON model. Applied once on
34
+ * mount and only when nothing has been restored from storage, so it seeds a
35
+ * first run without overwriting a layout the user has since arranged.
36
+ *
37
+ * Ignored when `layoutModel` is provided, which takes control outright.
38
+ */
39
+ initialLayoutJson?: IJsonModel;
40
+ /**
41
+ * Stop persisting the layout to `localStorage`. The workspace then resets to
42
+ * `initialLayoutJson` on every load — useful for embedded or multi-tenant
43
+ * shells where a per-browser layout would be wrong. Defaults to `false`.
44
+ */
32
45
  disableLocalStorage?: boolean;
33
- initialLayoutJson?: any;
34
- onLayoutChange?: (layout: any) => void;
46
+ /**
47
+ * Called with the serialised model whenever the user moves, splits, opens or
48
+ * closes a tab. Use it to persist the layout somewhere of your own — a user
49
+ * profile on the server, say — instead of, or alongside, `localStorage`.
50
+ */
51
+ onLayoutChange?: (layout: IJsonModel) => void;
35
52
  }
36
53
  /**
37
54
  * The complete application shell: menu bar, activity bar, sidebar, a
38
- * `flexlayout-react` docking area, terminal, chat pane and status bar.
55
+ * `flexlayout-react` docking workspace, terminal, chat pane and status bar.
56
+ *
57
+ * **This is where an application starts.** Call {@link initializeShell} once to
58
+ * register your commands, menus and panels, render this component, and add
59
+ * features by registering them rather than by restructuring the layout. Tab
60
+ * contents resolve through the {@link componentRegistry}, so a new view is a
61
+ * registration and a menu entry — the shell never needs to import it.
39
62
  *
40
- * This is the batteries-included composition. It registers whatever you pass
41
- * into the shell stores on mount, then renders the `Connected*` component
42
- * variants, which read from those stores. Tab contents come from the
43
- * {@link componentRegistry}, so plugins can contribute views without the shell
44
- * importing them.
63
+ * Configuration can arrive either way. Props are convenient for values that
64
+ * change with your app's state; {@link initializeShell} is better for the fixed
65
+ * set, since it runs once and works outside React. Props passed here are
66
+ * written into the same stores on mount, so the two are interchangeable.
45
67
  *
46
- * If you want the pieces without the wiring, import the individual components
47
- * and compose them yourself — every one of them is prop-driven and works
48
- * standalone.
68
+ * Internally this composes the `Connected*` component variants, which read from
69
+ * the shell stores. Those components — and the pure ones beneath them — are
70
+ * exported too, for the cases where you need to rearrange the frame itself.
49
71
  *
50
72
  * Closing a tab marked dirty via `useLayoutStore().setTabDirty` prompts for
51
73
  * confirmation before the tab is removed.
52
74
  *
53
75
  * @example
54
76
  * ```tsx
55
- * <ShellLayout
56
- * title={<Logo />}
57
- * panels={[{ id: 'files', label: 'Explorer', icon: Files, component: FileTree }]}
58
- * menuConfig={{ File: [{ id: 'save', label: 'Save', commandId: 'file.save' }] }}
59
- * statusBarConfig={[{ id: 'branch', label: 'main', alignment: 'left' }]}
60
- * />
77
+ * // main.tsx — a complete application
78
+ * import { initializeShell, ShellLayout, componentRegistry, AppTitle } from 'nexus-shell';
79
+ * import 'nexus-shell/style.css';
80
+ *
81
+ * componentRegistry.register('editor', Editor);
82
+ *
83
+ * initializeShell({
84
+ * panels: [{ id: 'files', label: 'Explorer', icon: Files, component: FileTree }],
85
+ * commands: [{ id: 'file.save', label: 'File: Save', keybinding: 'Control+s', execute: save }],
86
+ * menus: { File: [{ id: 'save', label: 'Save', commandId: 'file.save' }] },
87
+ * statusBar: [{ id: 'branch', label: 'main', alignment: 'left' }],
88
+ * });
89
+ *
90
+ * createRoot(document.getElementById('root')!).render(
91
+ * <ShellLayout title={<AppTitle title="Acme Studio" icon={<Boxes size={16} />} />} />,
92
+ * );
61
93
  * ```
62
94
  */
63
- export declare const ShellLayout: ({ panels, slashCommands, menuConfig, statusBarConfig, title, rightMenuBarContent, centerMenuBarContent, layoutModel, }: ShellLayoutProps) => import("react/jsx-runtime").JSX.Element;
95
+ export declare const ShellLayout: ({ panels, slashCommands, menuConfig, statusBarConfig, title, rightMenuBarContent, centerMenuBarContent, layoutModel, initialLayoutJson, disableLocalStorage, onLayoutChange, }: ShellLayoutProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,66 @@
1
+ import { ICommand } from './registry/CommandRegistry';
2
+ import { IMenuItemConfig } from './registry/MenuRegistry';
3
+ import { ISlashCommandConfig } from './services/ChatService';
4
+ import { ISidebarPanel } from './services/SidebarService';
5
+ import { IStatusBarWidgetConfig } from './services/StatusBarService';
6
+
7
+ export interface InitializeShellOptions {
8
+ /**
9
+ * Sidebar panels to register. Each becomes an icon in the activity bar and
10
+ * renders its `component` in the sidebar when selected.
11
+ */
12
+ panels?: ISidebarPanel[];
13
+ /** Menus keyed by top-level name. Items dispatch through their `commandId`. */
14
+ menus?: Record<string, IMenuItemConfig[]>;
15
+ /** Commands available to the palette, menus and keybindings. */
16
+ commands?: ICommand[];
17
+ /** Items for the status bar, grouped by their `alignment`. */
18
+ statusBar?: IStatusBarWidgetConfig[];
19
+ /** Slash commands offered in the chat pane. */
20
+ slashCommands?: ISlashCommandConfig[];
21
+ /**
22
+ * Register the built-in view and theme commands listed in
23
+ * {@link DEFAULT_COMMAND_IDS}. Defaults to `true`. Turn it off if your app
24
+ * wants to own every command id.
25
+ */
26
+ defaultCommands?: boolean;
27
+ /**
28
+ * Register a default **View** menu exposing the built-in commands. Defaults
29
+ * to `true`, and is ignored when `defaultCommands` is `false`.
30
+ */
31
+ defaultMenus?: boolean;
32
+ }
33
+ /** The command ids `initializeShell` registers unless told not to. */
34
+ export declare const DEFAULT_COMMAND_IDS: readonly ["view.toggleTerminal", "view.toggleChat", "view.toggleSidebar", "theme.light", "theme.dark", "theme.gt"];
35
+ /**
36
+ * Boot the shell: register commands, menus, panels and status bar items into
37
+ * the shell registries and stores.
38
+ *
39
+ * This is the usual first call in a Nexus Shell application. `ShellLayout` can
40
+ * take the same configuration as props, but doing it here has two advantages:
41
+ * it runs once rather than on every render, and it happens outside React, so
42
+ * plugins and other non-component code can contribute in exactly the same way.
43
+ *
44
+ * Call it once, before rendering. It is safe to call again — the registries
45
+ * warn on duplicate ids rather than throwing — but it is not a reset: nothing
46
+ * previously registered is removed.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * // main.tsx
51
+ * import { initializeShell, componentRegistry } from 'nexus-shell';
52
+ * import 'nexus-shell/style.css';
53
+ *
54
+ * componentRegistry.register('editor', Editor);
55
+ *
56
+ * initializeShell({
57
+ * panels: [{ id: 'files', label: 'Explorer', icon: Files, component: FileTree }],
58
+ * commands: [{ id: 'file.save', label: 'File: Save', keybinding: 'Control+s', execute: save }],
59
+ * menus: { File: [{ id: 'save', label: 'Save', commandId: 'file.save' }] },
60
+ * statusBar: [{ id: 'branch', label: 'main', alignment: 'left' }],
61
+ * });
62
+ *
63
+ * createRoot(el).render(<ShellLayout title={<AppTitle title="Acme" />} />);
64
+ * ```
65
+ */
66
+ export declare const initializeShell: (options?: InitializeShellOptions) => void;
package/dist/index.d.ts CHANGED
@@ -2,19 +2,45 @@
2
2
  * Nexus Shell — a React component library for building IDE-style application
3
3
  * shells.
4
4
  *
5
- * The surface is deliberately two-tier:
5
+ * **Start with {@link ShellLayout}.** It is the whole application frame — menu
6
+ * bar, activity bar, sidebar, docking workspace, terminal, chat pane and status
7
+ * bar — and the intended starting point for an app built on this library. Call
8
+ * {@link initializeShell} once to register your commands, menus and panels,
9
+ * render `ShellLayout`, and grow from there.
6
10
  *
7
- * 1. **Primitives** — pure, prop-driven components with no global state. Use
8
- * these to build your own layout. They are the supported public API.
9
- * 2. **Shell** — `ShellLayout` plus the stores and registries behind it, for
10
- * when you want the whole assembled application frame.
11
- *
12
- * Import the stylesheet once in your app entry:
13
11
  * ```ts
12
+ * import { initializeShell, ShellLayout } from 'nexus-shell';
14
13
  * import 'nexus-shell/style.css';
14
+ *
15
+ * initializeShell({ panels, menus, commands });
16
+ * createRoot(el).render(<ShellLayout title={<AppTitle title="Acme" />} />);
15
17
  * ```
18
+ *
19
+ * Everything the shell is built from is also exported on its own. Those
20
+ * components are pure and prop-driven, and they are the escape hatch: reach for
21
+ * them when you are embedding one piece into an app you already have, or when
22
+ * you need a layout the shell cannot express. Most applications should not need
23
+ * to.
16
24
  */
25
+ export { ShellLayout, type ShellLayoutProps } from './components/layout/ShellLayout';
26
+ export { initializeShell, DEFAULT_COMMAND_IDS, type InitializeShellOptions, } from './core/Boot';
27
+ export { commandRegistry, CommandRegistry, type ICommand } from './core/registry/CommandRegistry';
28
+ export { menuRegistry, MenuRegistry, type IMenuItemConfig } from './core/registry/MenuRegistry';
29
+ export { pluginRegistry, type IPlugin, type PluginStatus } from './core/registry/PluginRegistry';
30
+ export { componentRegistry, type ComponentConstructor } from './core/registry/ComponentRegistry';
31
+ export { useChatStore, type ISlashCommandConfig } from './core/services/ChatService';
32
+ export { useLayoutStore } from './core/services/LayoutService';
33
+ export { useModalStore, type ModalType as ModalStoreType } from './core/services/ModalStoreService';
34
+ export { useRightSidebarStore } from './core/services/RightSidebarService';
35
+ export { useSidebarStore, type ISidebarPanel } from './core/services/SidebarService';
36
+ export { useStatusBarStore, type IStatusBarWidgetConfig } from './core/services/StatusBarService';
37
+ export { useTerminalStore } from './core/services/TerminalService';
38
+ export { useThemeStore, type ThemeType } from './core/services/ThemeService';
39
+ export { useUserProfileStore, type UserProfileState } from './core/services/UserProfileService';
40
+ export { useKeyboardShortcuts } from './core/services/KeyboardService';
41
+ export * from './connected';
17
42
  export { ActivityBar, type ActivityBarProps, type IActivityBarItem } from './components/widgets/ActivityBar';
43
+ export { AppTitle, type AppTitleProps } from './components/widgets/AppTitle';
18
44
  export { ChatPane, type ChatPaneProps, type IChatMessage, type ISlashCommand } from './components/widgets/ChatPane';
19
45
  export { CommandPalette, type CommandPaletteProps, type ICommandItem } from './components/widgets/CommandPalette';
20
46
  export { ContextMenu, type ContextMenuProps, type IContextMenuItem } from './components/widgets/ContextMenu';
@@ -30,22 +56,4 @@ export { TerminalPane, type TerminalPaneProps } from './components/widgets/Termi
30
56
  export { ThemeSwitcher, type ThemeSwitcherProps, type IThemeOption, DEFAULT_THEME_OPTIONS } from './components/widgets/ThemeSwitcher';
31
57
  export { TreeWidget, type TreeWidgetProps, type ITreeNode, type ITreeAction, type ITreeContext } from './components/widgets/TreeWidget';
32
58
  export { UserProfile, type UserProfileProps, type IUserProfile, type IUserProfileAction } from './components/widgets/UserProfile';
33
- export { AppTitle, type AppTitleProps } from './components/widgets/AppTitle';
34
59
  export { cn } from './lib/cn';
35
- export { ShellLayout, ShellLayout as NexusWorkspaceShell, type ShellLayoutProps } from './components/layout/ShellLayout';
36
- export declare const initializeShell: (_config?: unknown) => void;
37
- export * from './connected';
38
- export { commandRegistry, CommandRegistry, type ICommand } from './core/registry/CommandRegistry';
39
- export { menuRegistry, MenuRegistry, type IMenuItemConfig } from './core/registry/MenuRegistry';
40
- export { pluginRegistry, type IPlugin, type PluginStatus } from './core/registry/PluginRegistry';
41
- export { componentRegistry, type ComponentConstructor } from './core/registry/ComponentRegistry';
42
- export { useChatStore, type ISlashCommandConfig } from './core/services/ChatService';
43
- export { useLayoutStore } from './core/services/LayoutService';
44
- export { useModalStore, type ModalType as ModalStoreType } from './core/services/ModalStoreService';
45
- export { useRightSidebarStore } from './core/services/RightSidebarService';
46
- export { useSidebarStore, type ISidebarPanel } from './core/services/SidebarService';
47
- export { useStatusBarStore, type IStatusBarWidgetConfig } from './core/services/StatusBarService';
48
- export { useTerminalStore } from './core/services/TerminalService';
49
- export { useThemeStore, type ThemeType } from './core/services/ThemeService';
50
- export { useUserProfileStore, type UserProfileState } from './core/services/UserProfileService';
51
- export { useKeyboardShortcuts } from './core/services/KeyboardService';