name: raykit-solid-lumino-bridge description: Bridge solid-js UI with @lumino/widgets and inversify in the Raykit browser runtime. Use when creating or refactoring browser-side widgets, renderer hosts, widget factories, application contributions, or service-driven communication between Solid views and external container state in Raykit packages and desktop browser code.
Raykit Solid Lumino Bridge
Use Raykit's existing browser primitives before adding new abstractions. Reuse BaseWidget, SolidWidget, SolidRenderer, WidgetFactory, WidgetService, and ApplicationBrowserContribution instead of inventing a parallel bridge.
Start Here
- Inspect the target package and confirm that the work belongs in
src/browser. - Read repo-map.md to anchor the implementation to existing code.
- Choose one rendering path:
- Use
SolidWidgetfor a full Lumino view or panel body. - Use
SolidRendererfor a specific host element that already exists.
- Use
- Put shared state and cross-widget communication into an injectable service.
- Register widget creation through
WidgetFactoryand open or attach the widget from anApplicationBrowserContribution. - Validate with
pnpm lint:fixandpnpm check.
Choose The Bridge
Use rendering-patterns.md when deciding between the two rendering primitives.
Default to SolidWidget when the Solid tree owns the widget body. SolidWidget already waits for Lumino attachment before rendering, which matches the current implementation in packages/widgets/src/browser/solid-widget.ts.
Use SolidRenderer when a Lumino widget or another browser object already owns the DOM node and you only need Solid to render inside a provided host. Treat it as a targeted rendering primitive. The repo currently defines this primitive, but it is not yet used by a business-facing example, so keep the usage explicit and narrow.
Drive State Through Services
Use communication-patterns.md for the state flow.
Keep inversify container access outside Solid components. Inject services into the widget or renderer, mirror service state into Solid signals or stores, and send UI actions back through service methods. Let Lumino messages handle widget lifecycle, not business events.
Register Through Browser Contributions
Use registration-patterns.md for container bindings.
Register the service, widget, and factory inside a browser ContainerModule. If the widget should appear in the default layout, implement an ApplicationBrowserContribution that requests the widget from WidgetService and adds it to app.shell.
Example
Use this example as the default end-to-end pattern for a new view. It covers module binding, widget creation, Solid rendering, service-driven updates, and shell attachment.
Call chain:
- The desktop browser bootstrap loads your browser module.
- The module binds the state service, widget class, factory contribution, and browser contribution.
ApplicationBrowser.start()runs contributions and then callsinitializeLayout.- The browser contribution asks
WidgetServicefor the widget. WidgetServiceresolves the factory, creates the widget, and caches it.- The contribution adds the widget to
app.shell. - Lumino attaches the widget,
SolidWidgetrenders the Solid tree, and service events keep the UI in sync.
1. Define a service as the data bridge
import { Emitter } from '@raykit/base'
import { injectable } from 'inversify'
export interface CounterSnapshot {
readonly count: number
}
@injectable()
export class CounterStateService {
protected snapshot: CounterSnapshot = { count: 0 }
protected readonly onDidChangeSnapshotEmitter = new Emitter<CounterSnapshot>()
readonly onDidChangeSnapshot = this.onDidChangeSnapshotEmitter.event
getSnapshot(): CounterSnapshot {
return this.snapshot
}
increment(): void {
this.update({ count: this.snapshot.count + 1 })
}
protected update(snapshot: CounterSnapshot): void {
this.snapshot = snapshot
this.onDidChangeSnapshotEmitter.fire(this.snapshot)
}
}
2. Mirror service state into a SolidWidget
import type { JSX } from 'solid-js'
import { SolidWidget } from '@raykit/widgets/browser'
import { inject, injectable } from 'inversify'
import { createSignal } from 'solid-js'
import { CounterStateService } from './counter-state-service'
function CounterPanel(props: { count: number, onIncrement: () => void }): JSX.Element {
return (
<div class="flex flex-col gap-3 p-4">
<strong>{props.count}</strong>
<button type="button" onClick={props.onIncrement}>Increment</button>
</div>
)
}
@injectable()
export class CounterWidget extends SolidWidget {
static readonly ID = 'example-counter-widget'
protected readonly countState = createSignal(0)
protected readonly count = this.countState[0]
protected readonly setCount = this.countState[1]
constructor(
@inject(CounterStateService)
protected readonly stateService: CounterStateService,
) {
super()
this.id = CounterWidget.ID
this.title.label = 'Counter'
this.setCount(this.stateService.getSnapshot().count)
this._register(this.stateService.onDidChangeSnapshot((snapshot) => {
this.setCount(snapshot.count)
}))
}
protected render(): JSX.Element {
return (
<CounterPanel
count={this.count()}
onIncrement={() => this.stateService.increment()}
/>
)
}
}
3. Register the service, widget, and factory in a browser module
import type { ApplicationBrowserContribution, WidgetFactory } from '@raykit/core/browser'
import { ApplicationBrowserContribution, WidgetFactory } from '@raykit/core/browser'
import { ContainerModule } from 'inversify'
import { CounterBrowserContribution } from './counter-browser-contribution'
import { CounterStateService } from './counter-state-service'
import { CounterWidget } from './counter-widget'
export const counterBrowserModule = new ContainerModule((options) => {
options.bind(CounterStateService).toSelf().inSingletonScope()
options.bind(CounterWidget).toSelf()
options.bind(CounterBrowserContribution).toSelf().inSingletonScope()
options.bind<WidgetFactory>(WidgetFactory).toDynamicValue(ctx => ({
id: CounterWidget.ID,
createWidget: () => ctx.get(CounterWidget),
})).inSingletonScope()
options.bind<ApplicationBrowserContribution>(ApplicationBrowserContribution)
.toDynamicValue(ctx => ctx.get(CounterBrowserContribution))
.inSingletonScope()
})
4. Attach the widget from an application contribution
import type { ApplicationBrowser, ApplicationBrowserContribution } from '@raykit/core/browser'
import { WidgetService } from '@raykit/core/browser'
import { inject, injectable } from 'inversify'
import { CounterWidget } from './counter-widget'
@injectable()
export class CounterBrowserContribution implements ApplicationBrowserContribution {
constructor(
@inject(WidgetService)
protected readonly widgetService: WidgetService,
) {}
async initializeLayout(app: ApplicationBrowser): Promise<void> {
const widget = await this.widgetService.getOrCreateWidget<CounterWidget>(CounterWidget.ID)
await app.shell.addWidget(widget, { area: 'main' })
widget.show()
}
}
5. Load the module in the browser bootstrap
import { ApplicationBrowser, applicationBrowserModule } from '@raykit/core/browser'
import { Container } from 'inversify'
import { counterBrowserModule } from './counter-browser-module'
const container = new Container()
container.load(applicationBrowserModule, counterBrowserModule)
await container.get(ApplicationBrowser).start()
6. Switch the same pattern to SolidRenderer only when you already have a host node
Keep the service and factory patterns unchanged. Replace CounterWidget extends SolidWidget with a host-owning Lumino widget plus a CounterRenderer that extends SolidRenderer. Inject RendererHost with that host node, move the signal subscription into the renderer, and keep UI callbacks writing back into the same service.
Avoid These Moves
- Do not call Solid
renderfrom a widget constructor. - Do not let Solid components fetch dependencies from the
inversifycontainer. - Do not use Lumino messages as a business event bus.
- Do not share mutable business state directly between widgets.
- Do not bypass
_registerordisposefor event subscriptions and cleanup.
Validate
- Run
pnpm lint:fix. - Run
pnpm check. - If you changed tests or added a new browser-side unit, run
pnpm exec vitest run <path>.