Skip to main content

Project structure

The creator generates a small project with three parts, whatever the framework:

  1. The plugin (plugin.ts): what gets built and published. It exports initialize and dispose.
  2. Its views: the components that the plugin injects into the shell regions.
  3. The sandbox (src/sandbox.ts): starts the demo shell with your plugin. It is only used in development and is not part of the plugin.
my-plugin/
├── index.html ← development page
├── src/
│ ├── plugin.ts ← initialize and dispose
│ ├── sandbox.ts ← starts the shell (dev only)
│ └── views/
│ ├── main-view.tsx ← a React component
│ └── main-view.css
├── vite.config.ts ← npm run dev / npm run build
├── tsconfig.json
└── package.json

The plugin: plugin.ts​

It is the entry point of the plugin, the only file the shell knows about. The shell calls initialize when it loads the plugin and dispose when it unloads it, each time with an API of its own (see the demo shell).

src/plugin.ts
import { wrapReactViewFactory } from "@uxland/harmonix-adapters";
import type { DemoShellApi } from "@uxland/harmonix-demo-shell";
import { MainView } from "./views/main-view";
import styles from "./views/main-view.css?inline";

export const initialize = async (api: DemoShellApi) => {
// A view in the `main` region: a React component rendered inside a web component.
await api.regionManager.registerMainView({
id: "main",
factory: () => wrapReactViewFactory(MainView, styles, { fullHeight: true })({ api }),
});
// An item in the side menu that activates that view.
await api.regionManager.registerNavigationItem({ id: "menu", label: "My plugin", mainViewId: "main" });
};

export const dispose = async (api: DemoShellApi) => {
const { regions } = api.regionManager;
await api.regionManager.removeView(regions.sideMenu, "menu");
await api.regionManager.removeView(regions.main, "main");
};

What it does:

  • A view is an object with an id and a factory: an async function that returns the HTML element to show. The shell calls the factory when the view has to be rendered. Whatever the framework, the shell only sees an HTML element.
  • registerMainView registers the view in the main region, and registerNavigationItem adds an item to the side menu that activates it. Both are shortcuts of the shell over the generic registerView(region, view).
  • The ids ("main", "menu") only have to be unique within the plugin: the shell stores the views as pluginId::viewId, so two plugins can use the same ids.
  • dispose undoes what initialize did. The shell can unload and reload plugins without reloading the page, so a plugin must not leave anything behind (see best practices).

The view​

The main view receives the API from plugin.ts, like any other input, and uses it to talk to other plugins through the broker: the button publishes a my-plugin:greeting event and the view, subscribed to it, counts the greetings it receives.

src/views/main-view.tsx
const GREETING = "my-plugin:greeting";

export const MainView = ({ api }: { api: DemoShellApi }) => {
const [greetings, setGreetings] = useState(0);

useEffect(() => {
const subscription = api.broker.subscribe(GREETING, () => setGreetings((count) => count + 1));
return () => subscription.dispose();
}, [api]);

return (
<section className="view">
{/* … */}
<button type="button" onClick={() => api.broker.publish(GREETING, { from: api.pluginInfo.pluginId })}>
Publish a greeting
</button>
<p className="result">Greetings received: {greetings}</p>
</section>
);
};

The view styles use the shell colors through CSS variables, such as var(--hx-primary-text, #00756a), with a fallback value in case another shell loads the plugin.

The sandbox: src/sandbox.ts​

It starts the demo shell and loads your plugin from its source code, so every change you save is reloaded. It is the only place where the plugin id is set:

src/sandbox.ts
import { bootstrapPlugins, initializeShell } from "@uxland/harmonix-demo-shell";

initializeShell(document.body, { title: "my-plugin" });
await bootstrapPlugins([{ pluginId: "my-plugin", importer: () => import("./plugin") }]);

bootstrapPlugins receives a list: to try your plugin together with others, add them to it. A real application does the same, but gets the list of plugins from the Plugin Store.

The configuration​

FileWhat it does
package.jsonThe scripts (dev, build, typecheck) and the dependencies. name and version identify the plugin when you publish it, and module points to the built file.
vite.config.tsReact and Lit: npm run dev serves index.html, and npm run build builds only src/plugin.ts, leaving the framework and the shell out of the bundle. Angular: the second step of the build (see Build and publish).
angular.jsonAngular only: the sandbox application, served by npm run dev, and the my-plugin library.
tsconfig.jsonTypeScript. Lit enables the decorators it needs.