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.
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
omitthat matched nothing, with the prefix you probably meant: omittingshell.permissionshides a command or item by that id, while the settings section of that name needssetting: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
omitremoved. 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’ssubmenu, by a registered toolbar, or as a surface’s own<id>/actionstoolbar. 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.
- Layout: regions and docks:
provideLayout, the regions and their docks, panes and sidebar curation. Why: Surfaces and panes. - Content-area routing:
provideShellRouter, the pane that carries the address, following tabs. Why: The address. - Workspaces a product ships:
provideWorkspaces, claims, rail items, unusable workspaces, saved workspaces. Why: Workspaces. - Resetting the arrangement: the workspace reset and the app layout reset, and what each puts back.
- Switching capabilities off:
provideShellFeatures, one switch per gesture and its affordance. - Surface retention: the product-wide
retentiondefault and the unsaved-work question. Why: Retention and unsaved work. - Branding:
provideProductIdentityand the--lw-*tenant theme. - Bringing your own CSS framework: the pre-compiled stylesheet, your framework in a cascade layer, the Bootstrap token mapping and the dark-mode mirror.
- Capabilities:
provideCapabilityGrants, the Permissions section,provideRequiredPlugins. Why: Capabilities and trust. - Auth integration:
provideAuthSource, your own login UI,provideUnauthorizedRedirect. - Persistence stores: the two
KeyValueStoreports, the storage-key inventory, identity-scoped stores. - Windows and sync: cross-tab live sync and pop-out windows, and where a product hooks in.
- Frame plugins:
provideFramePlugins, the frame kit you serve, your CSP. Why: Capabilities and trust. - Plugin store:
providePluginCatalog, the catalogue, consent and updates. - Icons, translations and rewording:
provideIcons, translation namespaces,provideTranslationOverrides. - Recomposing host chrome: replacing, hiding and moving default chrome, the palette entry, curating settings, dropping a route.
- PWA and delivery: the service worker, the manifest, and validating the update flow against a build.
Distribution API is what your own code can do once the product runs.