Skip to content

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.

KeysOpens
Cmd/Ctrl + Bthe navigation rail (Navigation)
Cmd/Ctrl + Kthe command palette (Global Search)
F1help 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.

php
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.

js
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:

blade
{{-- 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.jsonwho the actor is, their abilities, the navigation tree, the Weave links (Context API)
GET /ui5/shell/{slug}/search.jsonthe command palette's query (Global Search)
POST /ui5/shell/{slug}/intend.jsonevery action the shell takes (Intent Dispatch)
manifest.json → laravel.ui5.shellthe 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.

php
// 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 thisYou get
return two root nodes, or nonemust 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 configurableprovides configuration but does not implement …
list a service that is not a shell contributormust 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:

SymptomCheck
no chrome at all, the app renders barewhether the host's two Blade seats are wired, and whether ui5/shell/{version}/main.esm.js answers 200
the rail is empty or absentcontext.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 nothingphp artisan ui5:help --all — the help index lives in storage/ and is not in your repository
everything renders, nothing responds to keysthe 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 ​