LeanShell Overview
Core serves your UI5 app on a bare page. The SDK puts a shell around it: a navigation rail, a command palette, a help viewer, and the identity of the person using it — with the server deciding what each of those may contain.
LeanShell is not a UI5 component. It is a small web-component bundle (main.esm.js) that the host page loads beside your app and that mounts its chrome into one <div>. Your app stays an ordinary UI5 app; nothing about it is shell-specific except one line in its component and one base class in its manifest.
| Keys | Opens |
|---|---|
Cmd/Ctrl + B | the navigation rail (Navigation) |
Cmd/Ctrl + K | the command palette (Global Search) |
F1 | help for whatever has focus (Help Surface) |
How an app gets into the shell
Five things have to be true. Four of them are host setup you do once.
1 — the app's manifest class extends AbstractSdkManifest.
use LaravelUi5\Sdk\Platform\AbstractSdkManifest;
class OrdersManifest extends AbstractSdkManifest { /* … */ }This is the opt-in, and it is deliberate: a standalone app extends Core's AbstractManifest and gets no shell. The SDK base implements the interface that the host page's shell include tests for, and it is what assembles the shell fragment into the app's manifest.json.
2 — the app's component initializes the facade.
Component.extend('com.acme.orders.Component', {
init() {
UIComponent.prototype.init.apply(this, arguments);
LaravelUi5.init(this).then(() => this.getRouter().initialize());
},
});The shell does not start itself. It replaces LaravelUi5.init and starts when your app calls it — so an app that skips init() gets no chrome at all, and its LaravelUi5.call() throws (the init rule).
3 — the host renders the two seats. Core's page calls @includeIf('ui5.head') and @includeIf('ui5.foot'), and your host's views fill them:
{{-- resources/views/ui5/head.blade.php --}}
@includeIfSdk('ui5::head')
{{-- resources/views/ui5/foot.blade.php --}}
@includeIfSdk('ui5::foot')@includeIfSdk renders only when the current artifact is an app whose manifest opted in — which is why a Core-only app on the same host still gets a bare page. The SDK's head pulls the stylesheet and the icon font; its foot writes <div id="lux-shell"> and loads the bundle.
4 — the assets are published. The bundle itself is served from the package at ui5/shell/{version}/main.esm.js and style.css, so it moves with every SDK release by itself. php artisan ui5:publish copies the icon fonts and the default avatar to public/sdk/. Re-run it on every deployment and after every SDK upgrade.
5 — the shell endpoints are resolvable. Add ShellContextArtifactResolver to artifact_resolvers in config/ui5.php (Configuration). It is what binds an artifact to the three shell routes. A resolver missing from that list does not degrade a feature — it makes the endpoint 404, which looks like a broken shell rather than a missing line of config.
The wire
The shell talks to the server over three artifact-scoped endpoints, plus the app's manifest:
GET /ui5/shell/{slug}/context.json | who the actor is, their abilities, the navigation tree, the Weave links (Context API) |
GET /ui5/shell/{slug}/search.json | the command palette's query (Global Search) |
POST /ui5/shell/{slug}/intend.json | every action the shell takes (Intent Dispatch) |
manifest.json → laravel.ui5.shell | the endpoints, hotkeys and options the bundle configures itself from |
{slug} is the app's namespace. The shell never hardcodes a URL: the manifest fragment carries the templates, and the client fills in the slug.
Help has its own three routes (document, table of contents, search index), deliberately outside the artifact-scoped stack — one compiled index serves every module (Help Surface).
One service, one root key
The shell fragment in the manifest is assembled from shell services, and the rule that keeps it composable is blunt: a service contributes exactly one root node of the fragment, and it owns that node.
// config/ui5.php
'shell' => [
'manifest' => [ /* static options: hotkeys, placeholders, log level */ ],
'services' => [
'discovery.context' => [],
'discovery.search' => ['limit' => 1000],
HelpShellContributor::class => [],
],
],Each key is an FQCN or a container binding name, and each value is that service's configuration. The service implements Ui5ShellContributorInterface, plus ConfigurableServiceInterface if it takes configuration at all.
The rules are enforced when the manifest is built, and every violation is a LogicException — not a silent omission:
| You do this | You get |
|---|---|
| return two root nodes, or none | must return exactly one shell root node, N given |
| return a key another service already contributed | … already exists and cannot be overridden |
| pass configuration to a service that is not configurable | provides configuration but does not implement … |
| list a service that is not a shell contributor | must implement … |
So a new shell capability is a class plus a config line, and a name collision is a startup error rather than a mystery in the browser.
Redeclaring a key replaces the whole subtree
The SDK's config is merged one level deep. If you declare shell, context or discovery in your own config/ui5.php, your subtree replaces the SDK's — there is no deep merge. Adding one collector means restating the SDK's own entries alongside yours. Copy the shipped default, then edit it.
When a capability is missing
The shell degrades per capability rather than as a whole: the global hotkeys are bound before its data is fetched, and each part reports its own failure at error level in the console. So a backend that answers half the requests gives you half a shell, not a dead page.
What a missing piece looks like:
| Symptom | Check |
|---|---|
| no chrome at all, the app renders bare | whether the host's two Blade seats are wired, and whether ui5/shell/{version}/main.esm.js answers 200 |
| the rail is empty or absent | context.json in the network tab — a 404 usually means the missing ShellContextArtifactResolver above, a 403 means the middleware refused |
| F1 says help is unavailable, search finds nothing | php artisan ui5:help --all — the help index lives in storage/ and is not in your repository |
| everything renders, nothing responds to keys | the console at error level; then whether your app actually calls LaravelUi5.init(this) |
What it doesn't do
- It is not a UI5 shell control. There is no
sap.ushell. The chrome is web components; your app is a component inside it, and the two speak over the facade only. - It does not know your app's routes. The rail and palette move the user with intents, and the server resolves them (Intent Dispatch).
- It does not gate your data. Everything the shell hides — an entry in the rail, a row in the palette — is a projection of what the server already decided. The enforcement is in the middleware, the dispatch, and the OData read gate.
See also
- Navigation: the rail and the
route.*i18n contract - Context API: the fixed schema of
context.json - Global Search · Help Surface · Intent Dispatch
- Configuration: the host-side keys named here