Skip to content

Building a distribution

This is a guide, not the contract. What the platform guarantees is specified under openspec/specs/. For this page: platform-composition · shell-layout. Where this page and a specification disagree, the specification is right, and that is a defect in this page: change the behaviour there, then explain it here.

A distribution is your product: a thin app that composes @loomweaver/shell + your weaver(s), declares a layout, grants capabilities and sets branding. It’s mostly one file: the composition root.

The composition root

Everything a distribution is lives in one providers array, in src/app/app.config.ts, the file ng new and Nx both generate. Every “add this provider” instruction in these guides means that array. src/main.ts, src/app/app.ts and src/app/app.html stay as generated, except that App renders <lw-shell />; see manual setup for those three files.

src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import {
provideShell, provideShellRouter, provideLayout, providePlugins,
provideTranslationNamespaces, provideCapabilityGrants, type ShellLayout,
} from '@loomweaver/shell';
import { provideProductIdentity } from '@loomweaver/plugin-sdk';
import { notesPlugin } from '@my/notes-weaver';
const layout: ShellLayout = {
regions: [
{ id: 'top-bar', type: 'bar', dock: 'top' },
{ id: 'primary', type: 'rail', dock: 'left' },
{ id: 'left-panel', type: 'panel', dock: 'left' },
{ id: 'main', type: 'content', dock: 'center' },
{ id: 'right-panel', type: 'panel', dock: 'right' },
{ id: 'status-bar', type: 'bar', dock: 'bottom' },
],
};
export const appConfig: ApplicationConfig = {
providers: [
provideShellRouter(), // content-area routing — replaces provideRouter([])
provideShell(),
provideLayout(layout),
provideProductIdentity({ name: 'Notes Studio', tagline: 'product.tagline', logoUrl: 'logo.png' }),
provideTranslationNamespaces('notes', 'product'),
// Default-deny: grant the weaver exactly what its manifest declares.
provideCapabilityGrants({ notes: ['contributions', 'ui', 'navigation'] }),
...providePlugins(notesPlugin), // variadic, returns an array — note the spread
],
};

That’s the whole product wiring. The shell renders the chrome; the weaver fills it.

Which door does my decision go through?

There is no single god-provider, on purpose: each decision has its own provider, so the same decision never has two doors. The whole surface, indexed by what you want, is one page in the reference: Composition: the provider surface. Everything your own code can do at runtime is the rest of that area: Distribution API.

Seeing what you composed

In dev mode the shell puts one function on the window. Call it from the browser console once your app has finished loading:

loomweaver.report()

It prints the version the workbench shows (the shell’s own, or the one you set), the regions your layout declares, the capabilities you switched off and the ids you omitted. Then it warns about the things that quietly land nowhere:

  • an omit that matched nothing, with the prefix you probably meant: omitting shell.permissions hides a command or item by that id, while the settings section of that name needs setting:shell.permissions, and the bare form fails in silence
  • a settings row replacement whose id matched no row, so it replaces nothing
  • a rail item, bar button, view action, settings button or menu entry pointing at a command no one registers, or one your own omit removed. The shell drops the control rather than drawing a dead one, and this says why it vanished
  • a menu entry aimed at a slot nothing declares. A slot is declared by a menu the shell draws, by a rail item, bar button or surface action naming it as its menu, by another menu entry’s submenu, by a registered toolbar, or as a surface’s own <id>/actions toolbar. The entry is kept and appears the moment the slot is declared, so this is also warned about once in the console after the composed plugins have finished activating, naming the entry, the slot and the plugin that contributed it. It is deliberately not warned about at registration time: the plugin that fills a slot may activate before the one that owns it
  • a keyboard shortcut two commands both claim, naming both and saying which one the shortcut actually runs. The chord goes to whichever registered last, so the other command’s menu entry goes on offering a shortcut that now does something else. That is the part worth seeing: nothing looks broken, it simply does the wrong thing. The comparison is on the chord the keyboard resolves, so it finds a clash between two commands that spelled the same shortcut differently

Two checks do not wait for the console, because they are already decidable at startup, and the report repeats both. A bar, rail or view contribution aimed at a region your layout does not declare, or declares with another anatomy, is warned about immediately. The warning names the regions of the right type that do exist. That is the status versus status-bar mistake, which otherwise ships a product whose status bar is simply empty. And a provideRequiredPlugins naming a plugin the distribution does not compose is warned about, because that declaration is then ignored.

The report exists in dev only; nothing of it reaches a production build.

The pages

Each page below holds one area a distribution configures, with the provider that does it. Where a decision needs the reasoning first, the page names the concept page under concepts/ that carries it.

Distribution API is what your own code can do once the product runs.