Zum Inhalt springen

Multi-Panel Layout

Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.

Status: Draft 1 (preview) — early access, not for production. The managed-layout API is shipped but not yet battle-tested on a production consumer. Field names, defaults, and validation rules may still change between minor releases. Pin to an exact CDN version until this label is removed. For nearly all applications the standard compat mode is the recommended production mode — reach for managed layout only when you genuinely need to compose the chrome itself.

The managed-layout mode replaces the standard Wippy chrome with a fully declarative panel tree. Instead of the fixed chat-and-sidebar shell, you describe a tree of named panels in your backend YAML. The Web Host assembles the layout at boot, validates it, and maintains it reactively at runtime. Panels can be resized, collapsed, swapped, added, and removed without a page reload.

The standard compat mode (the default) gives you the fixed Wippy product: nav sidebar, chat panel, page area, and a right artifact panel. It is the current, most-used production mode and is sufficient for nearly all applications.

Opt in to fe_mode = managed (early access) only when you need to compose the chrome itself:

NeedCompatManaged
Standard Wippy chat + navYesReplaceable
Multiple page slots side by sideNoYes
Custom sidebar or coordinator componentLimitedYes — any panel kind
Responsive layouts per breakpointNoYes
Floating overlay panelsNoYes
Headless background service componentNoYes (services)
Per-panel URL-aware routingMain panel onlyEvery kind: page panel
Cross-panel message busNoYes (broadcast/send/on)
ComponentMinimum version
Wippy Web Host1.0.36
wippy-framework facade1.0.36
@wippy-fe/proxy0.0.36
@wippy-fe/webcomponent-core / @wippy-fe/webcomponent-vue0.0.36
@wippy-fe/layout / @wippy-fe/vue-host0.0.36

Pin to an exact CDN tag — at least https://web-host.wippy.ai/webcomponents-1.0.36 — until the Draft 1 label is removed.

Enable the managed entry in your facade configuration and provide a hostConfig.layout declaration:

hostConfig:
layout:
layouts:
default:
direction: horizontal
children:
- panel: nav
size: 240px
- panel: main
size: 1fr
main: true
panels:
nav: { kind: builtin, id: '@HOST/nav-sidebar' }
main: { kind: page, id: home }

When the managed entry is selected, the facade serves managed-layout.js instead of module.js. fe_mode is a current facade requirement parameter (default compat, opt-in managed); it is set on the wippy.facade requirement, not carried inside the AppConfig payload. There is no AppConfig.feature field — the managed layout is conveyed to the child entirely through AppConfig.hostConfig.layout. The proxy API surface is identical in both modes, but some commands only take effect in one mode — see What works in which mode.

The entire layout is described by a single HostLayoutDeclaration object nested under hostConfig.layout in your facade configuration. The host validates it before mounting — any LayoutValidationError surfaces in the browser console with { kind, message, panelId? }.

FieldTypeDescription
layoutsRecord<string, PanelTree> & { default: PanelTree }Breakpoint-keyed panel trees. The default key is required.
breakpoints?Record<string, number>Pixel widths that activate non-default layout keys.
panelsRecord<string, HostPanelDef>Named panel content definitions.
floating?Record<string, HostFloatingDef>Boot-time floating overlay panels.
modals?Record<string, HostModalDef>Boot-time modal definitions.
services?Record<string, HostServiceDef>Headless coordinator components.
dragEnabled?booleanAllow user-driven splitter drag. Default true.

Each entry in panels, floating, modals, and services is a tagged union on kind:

KindDescriptionRequired fields
pageA Wippy page module mounted in a srcdoc iframeid (page registry id)
artifactA Wippy artifact mounted in a srcdoc iframeid (artifact UUID)
componentA web component mounted directly in host DOMtagName
builtinA framework-owned host component (see below)id

Exactly one panel in the layout tree must carry main: true. That panel owns the host’s public URL via the mountRoute system. All other panels route independently inside their iframes.

kind: builtin accepts the following id values. The @HOST/ prefix is reserved for framework-owned panels:

IDWhat it renders
@HOST/nav-sidebarStandard Wippy nav sidebar (sessions, pages, settings)
@HOST/chat-wrapperStandard Wippy chat panel for the active session
@HOST/artifact-viewerGeneric artifact viewer (pair with route /:uuid)
@HOST/session-selectorSession list and picker

An unknown @HOST/<id> causes a LayoutValidationError at declaration-load rather than silently rendering an empty slot.

The layouts field maps breakpoint keys to panel trees. default is always used unless a narrower breakpoint matches. Breakpoint pixel widths are defined under breakpoints:

hostConfig:
layout:
breakpoints:
sm: 768
layouts:
default:
direction: horizontal
children:
- panel: side
size: 300px
- panel: main
size: 1fr
main: true
sm:
direction: vertical
children:
- panel: main
size: 1fr
main: true
- panel: side
display: drawer-left
drawerSize: { width: 320px }
panels:
side: { kind: page, id: app-sidebar, route: / }
main: { kind: page, id: app-home, route: / }

When the breakpoint changes, the active layout switches synchronously and panels with the same id in both layouts are teleported (not remounted) — iframe contentWindow, Vue state, and scroll position are preserved across the transition.

A panel slot can declare display: 'drawer-left' | 'drawer-right' | 'drawer-bottom' to render as a slide-in overlay instead of an inline flex item. Drawer panels:

  • Do not participate in their parent container’s track sizing (size is ignored)
  • Render as absolutely-positioned overlays anchored to the named edge
  • Have an open/close state toggled via host.layout.openDrawer(id) / closeDrawer(id) / toggleDrawer(id)
  • Show a backdrop when open; clicking the backdrop closes all open drawers

main: true slots cannot be drawer-mode — host validation throws. The drawerSize.width field controls the width for left/right drawers; drawerSize.height for bottom drawers. Default is 320px.

Floating panels are free-positioned overlays declared under floating. They do not participate in the flex layout tree and can be added or removed at runtime:

floating:
flap:
kind: component
tagName: my-right-flap
position: { x: 0, y: 200 }
size: { width: 48, height: 80 }

Runtime management:

// Add a floating panel
host.layout.addFloating('inspector', {
kind: 'component',
tagName: 'my-inspector',
position: { x: 100, y: 100 },
size: { width: 400, height: 300 },
})
// Remove it
host.layout.removeFloating('inspector')

Services are coordinator components mounted in a hidden div. They have no visible slot but receive the full proxy API. Use them for cross-cutting logic (translating bus events to panel updates, managing WebSocket subscriptions) so display panels stay focused on rendering:

services:
coordinator:
kind: component
tagName: my-coordinator

A service component receives the panel-scoped host wrapper and can subscribe to bus channels immediately in onMount:

import { WippyElement } from '@wippy-fe/webcomponent-core'
class MyCoordinator extends WippyElement {
protected onMount() {
this.host?.layout.on('open-chat', ({ payload }) => {
this.host?.layout.updatePanel('right', { route: `/open-chat/${payload.token}` })
this.host?.layout.expandPanel('right')
})
}
protected onUnmount() {}
static get wippyConfig() { return { propsSchema: { properties: {} } } }
}
customElements.define('my-coordinator', MyCoordinator)

Panels communicate through a bus scoped to the current browser tab. The bus never crosses to other tabs — use a custom WebSocket topic if you need multi-tab sync.

MethodDescription
host.layout.broadcast(channel, payload)Publish to all panels; sender excluded
host.layout.send(targetPanelId, channel, payload)Publish to one specific panel
host.layout.on(channel, handler)Subscribe; returns off() unsubscribe function

The sourcePanelId on received messages is set by the host from the publishing window and cannot be spoofed. Channel names are case-sensitive plain strings.

Important: Components that import host directly from @wippy-fe/proxy bypass panel scoping — bus calls go through but lose sourcePanelId. Always use the panel-scoped wrapper instead:

// raw HTMLElement
import { getWippyHost } from '@wippy-fe/webcomponent-core'
const host = getWippyHost(this)
// WippyElement subclass — this.host is already panel-scoped
this.host?.layout.broadcast('open-chat', { token: 'abc' })
// Vue component
import { useHost } from '@wippy-fe/webcomponent-vue'
// ProxyApiInstance is an ambient global type (from @wippy-fe/types-global-proxy) — reference it without an import.
const host = useHost<ProxyApiInstance['host']>()
host?.layout.broadcast('open-chat', { token: 'abc' })
MethodDescription
.snapshotSynchronous getter returning the full layout snapshot, or null outside managed-layout mode
.resizePanel(id, size)Resize the named panel in the active breakpoint
.collapsePanel(id)Collapse a panel declared collapsible: true
.expandPanel(id)Expand a collapsed panel
.openDrawer(id)Open a drawer-mode panel
.closeDrawer(id)Close a drawer-mode panel
.toggleDrawer(id)Toggle a drawer-mode panel
.movePanel(id, target)Move panel to a new tree position
.removePanel(id)Remove panel from all breakpoint layouts
.updatePanel(id, def)Patch panel definition at runtime; props shallow-merges, top-level fields replace
.addFloating(id, def)Add a floating panel
.removeFloating(id)Remove a floating panel
.openModal(id, def)Open a runtime modal (HostModalDef). Renders via native <dialog>.showModal() by default (top-layer, focus trap, inert backdrop); pass def.useNativeDialog: false for the legacy div-overlay path. Re-opening an open id is a silent no-op.
.closeModal(id)Close an open modal
.broadcast(channel, payload)Publish to all panels
.send(target, channel, payload)Publish to one panel
.on(channel, handler)Subscribe to a bus channel

host.layout.updatePanel(id, def) patches an existing panel def — it does not replace it. The props object is shallow-merged into the panel’s current props: supplied keys are added or overwritten, omitted keys are preserved. Every other top-level field of def (route, kind, id, tagName, title, icon, …) replaces the current value wholesale.

Given a panel whose current props are { artifactId: 'old', zoom: 2 }:

// props shallow-merges → { artifactId: 'abc', zoom: 2 }
host.layout.updatePanel('right', { props: { artifactId: 'abc' } })
// route replaces wholesale; props left untouched
host.layout.updatePanel('right', { route: '/x' })

Two caveats: the props merge is shallow — a nested object inside props is replaced entirely, not deep-merged — and a shallow merge cannot delete a prop key (you can only overwrite it).

These composables wrap the proxy layout API in reactive Vue 3 refs. The underlying subscription is module-scoped and lives for the iframe’s lifetime, so there is no per-component cleanup on unmount:

ComposableReturns
useWippyLayout()Full layout state and mutation methods
useWippyPanel(panelId)Named panel’s live state (panelId is required — string, Ref<string>, or getter)
useWippyBreakpoint()Active breakpoint name as a reactive ref
useWippyMainRoute()Reactive ref to the main panel’s current route

The composables never return null — they always hand back objects/refs whose inner .value degrades when no managed-layout host is present: useWippyLayout().snapshot.value is null (and isManaged.value is false, so mutations are silent no-ops), useWippyBreakpoint().value and useWippyMainRoute().value are empty strings, and useWippyPanel(id).value is null when the id is absent. Guard host presence with layout.isManaged.value (or layout.snapshot.value !== null) rather than a === null check on the return value. This keeps the composables usable in standalone playgrounds and unit tests where no managed-layout host is present.

The proxy API surface is identical in compat and managed mode — the same @wippy-fe/proxy imports resolve in both — but two parts of it are mode-specific in effect. This mismatch is the main thing to watch when moving an app onto managed layout (and a reason managed is still early access).

host.layout takes effect only in managed mode

Section titled “host.layout takes effect only in managed mode”

The host installs the layout receiver only when a layout is declared (the managed entry, gated on hostConfig.layout). In compat mode host.layout still exists, but host.layout.snapshot is null and every mutation and bus call (resizePanel, updatePanel, movePanel, openModal, addFloating, broadcast, send, on, …) is a silent no-op — the message is posted but nothing on the host is listening. Gate on the snapshot before mutating:

if (host.layout.snapshot) {
host.layout.updatePanel('right', { route: '/details' }) // managed only
}
// Vue: const { isManaged } = useWippyLayout(); if (isManaged.value) { … }

(Separately — a different axis — addPanel and setLayout are not exposed over the proxy at all, in either mode; see Known Limitations.)

host.* commands that assume the compat shell

Section titled “host.* commands that assume the compat shell”

The managed shell renders only your declared layout — it mounts no built-in nav sidebar, right artifact panel, modal host, or root <RouterView>. So host.* commands whose visible effect lands in that standard chrome have nowhere to render in managed mode unless you declare a panel for it:

host.* commandCompat (default)Managed
setContext, toast, confirm, handleError, logout, bridge.*, top-level state / ws / onWorksWorks — mode-agnostic (managed mounts <Toast> / <ConfirmDialogTemplate> so toast/confirm still surface)
openArtifact(id, …)Opens in the right panel or a modalNo visible effect — managed mounts no right panel or modal host
startChat(token) / openSession(uuid)Opens the chat session and shows itThe session really opens over WebSocket, but nothing renders — there is no chat route or right panel. Declare a kind: page panel bound to the session to display it
navigate(url)Pushes the route under the host’s root <RouterView>The route changes but does not render — managed has no root <RouterView>. Drive panels via mountRoute / updatePanel instead
onRouteChanged(route, navId?)Drives the host browser URLWorks, different semantics: the host writes the child’s route into that panel’s live state (the snapshot), not the browser URL

Rule of thumb: the WebSocket / state / bus / toast primitives are mode-agnostic, but anything that shows the standard Wippy chrome (chat, right-panel artifacts, top-level routing) is effectively compat-only. In a managed layout, render those through declared panels rather than the shell commands.

Three tiers, in order of preference:

Route — If the user could meaningfully bookmark or share the state, put it in the URL. Each kind: page panel runs its own router and reacts to @history events. This is decoupled, deep-linkable, and browser-history-aware.

Layout snapshot — If it affects layout shape (sizes, collapsed flags, component props), put it in the snapshot via updatePanel or resizePanel. Every subscribed panel sees every snapshot change, so keep payloads small.

Panel-local — Everything else (form drafts, modal state, transient UI) stays inside the panel’s own Pinia stores or refs and never leaves the panel.

The recommended pattern for cross-panel interaction is: bus event → coordinator service → updatePanel → panel reacts via its own router.

// In the coordinator service
this.host?.layout.on('open-chat', ({ payload }) => {
this.host?.layout.updatePanel('right', { route: `/open-chat/${payload.token}` })
this.host?.layout.expandPanel('right')
})
// In the right-panel app (a normal Vue page module)
const router = createAppRouter([...])
// createAppRouter already mirrors host @history events into the router
// (with an echo/current-route guard) — no manual on('@history') needed.

Keep coordinators thin. Keep panels owning their own UI.

As of Draft 1, the following are not yet implemented:

  • addPanel / setLayout over the proxy — not shipped. These exist only on the internal @wippy-fe/layout LayoutManager and are not exposed across the iframe proxy boundary. (openModal, closeModal, and movePanel are shipped — see the Layout API Reference.)
  • Panel drag-to-rearrange UI — the data model and movePanel() API work; user-facing drag is not yet implemented.
  • Tab primitive — not yet implemented.
  • Grid-tile container — tracked for a follow-up.
  • Runtime mutation persistence — mutations are not persisted across reloads. Persist manually if needed:
    on('@layout-change', () =>
    state.set('layout', host.layout.snapshot)
    )
  • nav-sidebar header slot extension points — logo, app-name, and toggle button positions are fixed in this draft.
  • Facade Entry Point — how the facade loads the JS-module entry and delivers config
  • Bootstrap Sequence — how the host dispatches to the managed-layout entry at boot
  • Packages@wippy-fe/layout, @wippy-fe/vue-host, @wippy-fe/webcomponent-core, @wippy-fe/webcomponent-vue