@hyperfrontend/features/hostHost
Host-side SDK for embedding hyperfrontend features: shell factory, display modes, iframe utilities, and lifecycle.
import { builtInDisplayModes, createShell, DisplayMode } from '@hyperfrontend/features/host'
const shell = createShell({
modes: builtInDisplayModes,
url: 'https://features.example.com/clock',
container: '#clock-slot',
displayMode: DisplayMode.Embedded,
})
shell.on('open', () => console.log('feature connected'))
shell.on('tick', (time) => console.log('feature said', time))
shell.open()
shell.send('set-timezone', { tz: 'UTC' })
API
| Export | Purpose |
|---|---|
createShell | Build a shell handle from an explicit modes map: only the mounts you pass ship. |
builtInDisplayModes | The all-modes map, for hosts that want every mode available. |
mountEmbedded … | The four mount functions (mountEmbedded, mountDialog, mountPopup, mountStandalone) for the modes map. |
DisplayMode | The four built-in modes: Embedded, Dialog, Popup, Standalone. |
ShellHandle | Type of the handle returned by createShell. |
CreateShellOptions | Options accepted by createShell (ShellOptions plus the modes map). |
ExperiencePlugin | Opt-in extension point for layering transitions/animations onto display modes. |
The modes map is how unused mode code stays out of bundles: a generated shell passes exactly the modes its feature declared, and a hand-written host that only ever embeds can pass { embedded: mountEmbedded } and ship one mode. Opening a mode outside the map throws, naming the supported set.
The shell wraps a @hyperfrontend/nexus broker: send emits a contract action to the feature, and on subscribes to feature messages and the open/closing/close/error/status/dirty-state/reopen/dismiss lifecycle events. close disconnects the channel politely (the feature gets a closing flush window, and isDirty reports declared unsaved work first); destroy also releases the DOM.
Opening is asynchronous: isOpen stays false and sends queue until the wire handshake with the feature completes, flushing on the open event. If the feature never completes the handshake within openTimeoutMs (default 10 s), the shell tears the mount down and emits error with reason: 'open-timeout'.
Every shell-originated error payload carries a reason discriminator:
| reason | Emitted when | Payload beyond reason |
|---|---|---|
'open-timeout' | The handshake does not complete within openTimeoutMs. | elapsedMs, displayMode |
'open-failed' | The display mode cannot produce a feature window at all (e.g. a blocked popup). | displayMode |
'target-lost' | A window the shell opened stops being reachable before the handshake completes. | elapsedMs, displayMode |
'unresponsive' | The heartbeat watchdog trips under the emit, unmount, or reopen onUnresponsive policy. | missedBeats, lastBeatAt, displayMode, frame |
'reopen-exhausted' | The reopen policy spent its attempts and the feature is still silent; the shell has torn it down. | attempts, displayMode |
'target-lost' has two possible causes and the browser gives the host no way to tell them apart: the visitor closed the window, or the feature document carried Cross-Origin-Opener-Policy: same-origin and the browser severed the opener as it loaded. The elapsedMs is what separates them in practice. A severed opener is reported as the document commits, typically within a few hundred milliseconds of open(); a visitor reaching for the close control takes far longer. A short elapsedMs on a popup or standalone session is worth reading as an isolation problem on the feature origin rather than a dismissal.
The watchdog cannot tell a frame that died from one that stalled: a renderer process the browser or the operating system killed (routine on phones) goes silent without any signal the embedding page can observe, exactly like a frame whose main thread is starved. The 'unresponsive' error says what the host can see through frame: 'gone' when the iframe left the page or the window was closed, 'present' otherwise. Only time separates the two 'present' cases, and the 'reopen' policy spends that time for you:
const shell = createShell({
modes: builtInDisplayModes,
container: '#clock',
url: 'https://clock.example.com',
onUnresponsive: {
reopen: { graceMs: 4000, backoff: 3, attempts: 3, stableMs: 60_000 },
},
})
shell.on('reopen', (data) => console.info('reviving the clock', data))
A frame that beats again within graceMs was only stalled and keeps its session. One that stays silent has its mount (the browser's crash placeholder included) replaced by a fresh one opened with the same options, announced by a 'reopen' event carrying { attempt, attempts, displayMode }. Each further death in the episode waits backoff times longer, and a reopened session that stays open for stableMs restores the whole budget. 'reopen' alone takes every default shown above.
Nothing is reopened into a hidden page: the attempt waits for the visitor to return, then allows a fresh grace. A reopened session that never completes its handshake counts as another death. The policy stands down for a 'gone' frame (the shell tears it down instead, since your page or the visitor removed it), when you call close, destroy, or open yourself, and when the feature closes the session.
A frame whose renderer died is painted over by the browser's own crash placeholder, which your styles cannot reach. Set concealUnresponsive to hide the frame from the verdict until its next beat or the next session opens. The trade-off: a feature that merely stalls past the miss budget also disappears until it beats again.
A feature that reloads itself (a refresh, an in-frame navigation, a dev-server rebuild) ends its session but keeps its mount: close fires with { reason: 'peer-reload' }, then open fires again once the new document completes its own handshake, and the shell re-announces the presentation to it. Treat the pair as a session boundary: pending requests reject, isDirty resets, and anything you sent the previous document needs sending again. To refuse the reload instead, destroy() on that reason.
Display modes and sizing
The host owns presentation. It picks the display mode (from the set the feature's contract declares), announces it to the feature once per mount (the announcement already carries the frame's initial dimensions, so the feature lays itself out without waiting for a second message), and is the single authority on frame geometry: every dimension crosses the boundary as an exact pixel value, host to feature, never the other way.
Mounted is not displayed: the frame mounts hidden and is revealed only once the session opens, so the user never sees (or clicks into) a frame whose feature is not ready. A dialog pane cannot intercept the page while it is still connecting; an embedded frame reserves its box without painting.
Embedded mounts the frame inline in your container and the frame fills the container's content box: measured before the frame is inserted, then observed with a ResizeObserver; every later change is reported to the feature. While the container has no measurable size (hidden, not yet laid out, or nothing gives it a height), the SDK applies a dynamic viewport-derived fallback so the embed is never invisible by accident; the fallback retires as soon as your layout takes over.
A feature with intrinsic dimensions can bake fixed embedWidth/embedHeight instead (or you can pass them), then the frame gets exactly those pixels and you place the container somewhere they fit; the SDK never distorts a fixed agreement.
Dialog is a full-viewport transparent pane layered above your page. The feature draws its dialog box (and any backdrop paint) inside the pane; dialogWidth/dialogHeight set the inner box (viewport-derived when unset) and dialogPosition places it: center by default, or any edge/corner (top-left … bottom-right). The feature detects backdrop clicks and in-frame Escape presses and reports them as a dismiss signal; dialogBackdrop decides what the shell does: close (default) starts the polite teardown, event emits a dismiss event for you to handle, none ignores it. closeOnEscape covers Escape from both documents.
shell.open({
displayMode: DisplayMode.Dialog,
dialogWidth: 480,
dialogPosition: 'top-center',
dialogBackdrop: 'event',
})
shell.on('dismiss', ({ source }) => console.log('backdrop interaction', source))
Popup opens a separate window at popupWidth/popupHeight (viewport-derived when unset), placed on the screen per popupPosition (center by default, or any edge/corner). After that the window belongs to the browser and the user: they move and resize it freely, and no frame geometry crosses the boundary. The window's title and chrome are not the host's to set: the title comes from the loaded document (the feature sets its own document.title), and browsers ignore chrome flags like resizability for window.open. Standalone is a plain new tab: the simplest mode, deliberately free of presentation coordination.
Both windowed modes depend on the opener relationship surviving the feature document's load, and a cross-origin isolated feature origin ends it. crossOriginIsolated (the precondition for SharedArrayBuffer and performance.measureUserAgentSpecificMemory) requires Cross-Origin-Opener-Policy: same-origin, and a window opened onto such an origin keeps its opener only when the opener is same-origin and itself isolated. Every other pairing puts the new window in its own browsing-context group: window.opener is null inside it, messages in both directions are discarded without error, and the session times out while the feature renders perfectly on screen.
Note that same-origin-allow-popups does not widen this, because it governs popups the document opens, not the opener it keeps when it is opened. Declare isolation in feature.config.* and the toolchain composes only the modes the origin can serve; the type narrowing means declaring isolation alongside popup or standalone does not compile.
Transparency is on by default in both iframe modes (allowtransparency plus a matched color-scheme pin on both sides; a mismatch would force an opaque canvas), so features can render non-rectangular designs, blend with your UI, and paint dialog backdrops. Want an opaque embed? Give your container a background.
What the SDK deliberately does not do
- Content-driven growth (the embed grows with its content) is not built in: it would hand geometry authority to the feature, which is the inversion this design exists to avoid. The recipe is one contract action: have the feature emit its content height as product data, and set it on the container you own (
shell.on('contentHeight', (px) => setContainerHeight(px))); the SDK's container observation propagates the change back down. - Clipping and scrolling are not managed. The feature owns its document's overflow behaviour; the host owns the container's. Neither needs a protocol.
- Draggable/resizable dialog boxes are not built in, but the full-viewport pane makes them a pure feature-side concern: the pane never moves, so the feature can drag or resize its inner box with ordinary CSS/pointer code and nothing needs to cross the boundary. If your host UI needs to know, carry position as an ordinary contract action.
Browser capabilities
Capability is one of the two axes in the Security Model: read it for where these options sit relative to what the browser enforces and what stays the operator's job.
Two ShellOptions fields govern what the feature frame may do with the browser around it, both applied before the frame loads (the only moment they take effect) and both scoped to the iframe modes: popup and standalone open top-level windows, which ask the user for permissions directly.
permissions delegates Permissions-Policy features (camera, fullscreen, clipboard, …) to the frame via the iframe allow attribute. Browsers deny these to cross-origin frames by default, so a feature that needs one only works when the host delegates it. A generated shell bakes the needs the feature declared at build time (also disclosed in its README and metadata.json); a host-supplied list replaces the baked one entirely.
sandbox is the host's containment lever and is never baked by a build. true (or an opt-in object) starts the frame from the browser's deny-all sandbox, and the SDK manages the two hazardous tokens itself:
allow-scriptsis always granted, because the feature runtime is JavaScript.allow-same-originis granted only to cross-origin feature URLs, since a same-origin frame holding both tokens could remove its own sandbox. A sandboxed same-origin feature therefore runs with an opaque origin (no cookies or storage) while the messaging protocol still connects.- Everything else (
forms,popups,modals,downloads,topNavigationByUserActivation) is denied unless opted in.
Requesting a sandbox on popup or standalone throws, because no containment can apply to a top-level window.
shell.open({
permissions: ['fullscreen', 'clipboard-write'],
sandbox: { downloads: true },
})API Reference§
ƒ Functions
Provisions a nexus broker and returns a handle whose
open mounts the feature in the requested display mode. The shell is built from an explicit modes map: pass the mounts this host supports (which is how generated shells exclude undeclared modes from their bundles) or builtInDisplayModes for all of them; opening a mode outside the map throws, naming the supported set. The contract option takes the feature's contract exactly as the feature authored it; the shell derives the host-side orientation itself, so the handle sends what the feature accepts and receives what the feature emits.Parameters
| Name | Type | Description |
|---|---|---|
§options | CreateShellOptions | Create-time options including the modes map; overridable per open call. |
Returns
ShellHandleopen, close, destroy, send, on, and isOpen.Example
Embedding a clock feature with every built-in mode available
const clock = createShell({ modes: builtInDisplayModes, container: '#clock', url: 'https://clock.example.com' })
clock.open({ displayMode: DisplayMode.Dialog, dialogWidth: 530 })
clock.on('timeUpdated', (data) => console.log(data))◈ Interfaces
Properties
closeOnEscape?:booleanWhether Escape closes the dialog; defaults to true. Enforced on both sides of the boundary: the host listens in its own document, and the feature reports an Escape pressed inside its frame as a dismiss signal the host acts on (dialog mode only).concealUnresponsive?:booleanWhether the shell hides the feature's frame on the unresponsive verdict; defaults to false. The frame stays mounted with its session open, and its next beat or the next session to open shows it again. This keeps the browser's crash placeholder for a dead frame off your page, but a frame that merely stalls past the miss budget also disappears until it beats again. Applies to the iframe modes (embedded, dialog) with any UnresponsivePolicy.container?:string | HTMLElementAnchor element (or CSS selector) the embedded feature mounts into; required by (and only meaningful for) embedded mode.contract?:FeatureContractThe feature's contract exactly as the feature authored it (emitted = what the feature sends, accepted = what the feature handles). The shell derives the host-side orientation itself: hand it the feature's contract, never a pre-swapped copy. Replaces the generic default when provided.dialogBackdrop?:BackdropBehaviorHow the host reacts to a pointer interaction on the dialog backdrop, the transparent area around the feature's dialog box; defaults to close. See BackdropBehavior.dialogHeight?:numberHeight in pixels of the feature's inner dialog box; see ShellOptions.dialogWidth.dialogPosition?:BoxPositionWhere the inner dialog box sits inside the pane (dialog mode only); defaults to center.dialogWidth?:numberWidth in pixels of the feature's inner dialog box (dialog mode only). Crosses the boundary at open and is applied by the hostee SDK inside the full-viewport dialog pane; when absent, the hostee derives a size from the viewport and its aspect ratio.embedWidth?:numberFixed embedded width in pixels. When both embedWidth and embedHeight are set, the embedded iframe receives exactly those dimensions instead of filling its container, and the host application is responsible for placing the container somewhere the feature fits: the SDK never distorts or reinterprets fixed dimensions. Setting only one of the pair throws.onUnresponsive?:UnresponsivePolicyHow the host reacts when the feature stops responding; defaults to emit.openTimeoutMs?:numberMilliseconds the shell waits for the feature to complete the connection handshake before emitting an error with reason: 'open-timeout' and tearing the mount down; defaults to 10000. Opening is asynchronous:
isOpen stays false and the open event fires only once the wire handshake completes. send/request calls issued in between queue on the channel and flush on open.permissions?:unknownPermissions-Policy features delegated to the feature frame, applied as the iframe allow attribute scoped to the frame's own origin. Shell builds bake the feature's declared needs here; a host-supplied list replaces the baked one entirely. Only the iframe modes (embedded, dialog) apply it: popup and standalone open top-level windows, which request these permissions from the user directly.plugins?:unknownExperience plugins wrapped around each mount/unmount; onMount runs in registration order, onUnmount in reverse.popupHeight?:numberPopup window height in pixels (popup mode only); when absent, derived from the viewport.popupPosition?:BoxPositionWhere the popup window sits on the screen (popup mode only); defaults to center.popupWidth?:numberPopup window width in pixels (popup mode only); when absent, derived from the viewport.sandbox?:boolean | SandboxOptionsContainment posture for the feature frame. true (or an opt-in object) starts the frame from the browser's deny-all sandbox; the SDK always returns allow-scripts, grants allow-same-origin only to cross-origin feature URLs, and denies everything else unless opted in; see SandboxOptions for the managed tokens and why. Host-decreed and never baked by a shell build. Only meaningful for the iframe modes: opening popup or standalone with a sandbox set throws, because no containment can apply to a top-level window.Properties
Properties
Properties
element?:HTMLElementIn-document root the mode mounted (the feature iframe); unset when the feature opens in a separate window.present:PresentPayloadPresentation announcement the shell sends the feature once per mount: the mode, the frame's initial dimensions, and any agreed dialog box geometry.viewport?:ViewportReporterReporter of the frame's exact pixel space, when the mode observes an iframe; the shell forwards its change reports once the channel opens.Properties
Created by a display-mode mount seeded with a synchronous initial measurement:
current() feeds the presentation announcement, and once the shell calls start, only changes relative to what was already announced are forwarded: the initial size never crosses twice.Properties
Structurally compatible with nexus's channel contract action shape so the same contract can drive both messaging and the shell type generator.
Properties
required?:booleanMarks an accepted action as essential for correct operation: the connection is denied at handshake time unless the counterpart emits this type. Only meaningful on accepted entries. Unflagged actions never gate the connection, so additive contract evolution stays non-breaking.respondsWith?:stringWhen this action is used as a request, the type of the action in the other direction that answers it.Register plugins through ShellOptions.plugins. After each successful mount the shell calls
onMount on every plugin in registration order; before each unmount it calls onUnmount one plugin at a time in reverse registration order, awaiting any returned promise, then runs the teardowns returned by onMount (also in reverse registration order) and finally removes the feature. The SDK ships no built-in plugins.Properties
Properties
element:HTMLElementThe in-document root the display mode mounted: the iframe for embedded, the dialog container for dialog, and null for popup and standalone, which open a separate window with no in-document element.This is the same shape the on-disk
*.contract.json files and the shell generator consume.Properties
version?:stringOptional semver version announcing the contract cut this side holds. Builds canonicalize and bake it into the generated shell; the two sides compare their announcements during the connection handshake and incompatible cuts are denied before the channel opens. Absent on either side, the check passes, so unversioned peers keep connecting.dialog.Properties
viewport?:ViewportPayloadThe frame's usable space at mount time, in exact pixels (iframe modes only), so the feature can lay itself out without waiting for the first viewport report; later changes arrive as viewport reports.reopen UnresponsivePolicy: how patiently, how often, and how many times the shell brings back a feature whose frame went silent. One episode starts at the first verdict and keeps counting while the feature keeps dying; a reopened session that stays open for
stableMs ends the episode and restores the full budget.Properties
attempts?:numberReopens one episode may spend; a positive integer, defaults to 3. A feature still silent after the last one is torn down and an error with reason: 'reopen-exhausted' is emitted.graceMs?:numberMilliseconds a verdict must stand before the first reopen; defaults to - A frame that beats again within the grace was stalled, not dead, and
backoff times longer than the one before.stableMs?:numberMilliseconds a reopened session must stay open before the episode ends and the budget is restored; defaults to 60000.reopen UnresponsivePolicy.Properties
request.Properties
Enabling ShellOptions.sandbox starts the frame from the browser's deny-all sandbox and returns capabilities selectively. Two tokens are managed by the SDK and are not configurable:
allow-scripts is always present (the feature runtime is JavaScript, so a script-less frame can never connect), and allow-same-origin is granted only when the feature URL resolves to a different origin than the host page: a same-origin frame holding both tokens could remove its own sandbox, so that pairing cannot be expressed. A sandboxed same-origin feature therefore runs with an opaque origin (no cookies or storage); the messaging protocol still connects. Every opt-in below defaults to false (denied).Properties
Properties
closeOnEscape?:booleanWhether Escape closes the dialog; defaults to true. Enforced on both sides of the boundary: the host listens in its own document, and the feature reports an Escape pressed inside its frame as a dismiss signal the host acts on (dialog mode only).concealUnresponsive?:booleanWhether the shell hides the feature's frame on the unresponsive verdict; defaults to false. The frame stays mounted with its session open, and its next beat or the next session to open shows it again. This keeps the browser's crash placeholder for a dead frame off your page, but a frame that merely stalls past the miss budget also disappears until it beats again. Applies to the iframe modes (embedded, dialog) with any UnresponsivePolicy.container?:string | HTMLElementAnchor element (or CSS selector) the embedded feature mounts into; required by (and only meaningful for) embedded mode.contract?:FeatureContractThe feature's contract exactly as the feature authored it (emitted = what the feature sends, accepted = what the feature handles). The shell derives the host-side orientation itself: hand it the feature's contract, never a pre-swapped copy. Replaces the generic default when provided.dialogBackdrop?:BackdropBehaviorHow the host reacts to a pointer interaction on the dialog backdrop, the transparent area around the feature's dialog box; defaults to close. See BackdropBehavior.dialogHeight?:numberHeight in pixels of the feature's inner dialog box; see ShellOptions.dialogWidth.dialogPosition?:BoxPositionWhere the inner dialog box sits inside the pane (dialog mode only); defaults to center.dialogWidth?:numberWidth in pixels of the feature's inner dialog box (dialog mode only). Crosses the boundary at open and is applied by the hostee SDK inside the full-viewport dialog pane; when absent, the hostee derives a size from the viewport and its aspect ratio.embedWidth?:numberFixed embedded width in pixels. When both embedWidth and embedHeight are set, the embedded iframe receives exactly those dimensions instead of filling its container, and the host application is responsible for placing the container somewhere the feature fits: the SDK never distorts or reinterprets fixed dimensions. Setting only one of the pair throws.onUnresponsive?:UnresponsivePolicyHow the host reacts when the feature stops responding; defaults to emit.openTimeoutMs?:numberMilliseconds the shell waits for the feature to complete the connection handshake before emitting an error with reason: 'open-timeout' and tearing the mount down; defaults to 10000. Opening is asynchronous:
isOpen stays false and the open event fires only once the wire handshake completes. send/request calls issued in between queue on the channel and flush on open.permissions?:unknownPermissions-Policy features delegated to the feature frame, applied as the iframe allow attribute scoped to the frame's own origin. Shell builds bake the feature's declared needs here; a host-supplied list replaces the baked one entirely. Only the iframe modes (embedded, dialog) apply it: popup and standalone open top-level windows, which request these permissions from the user directly.plugins?:unknownExperience plugins wrapped around each mount/unmount; onMount runs in registration order, onUnmount in reverse.popupHeight?:numberPopup window height in pixels (popup mode only); when absent, derived from the viewport.popupPosition?:BoxPositionWhere the popup window sits on the screen (popup mode only); defaults to center.popupWidth?:numberPopup window width in pixels (popup mode only); when absent, derived from the viewport.sandbox?:boolean | SandboxOptionsContainment posture for the feature frame. true (or an opt-in object) starts the frame from the browser's deny-all sandbox; the SDK always returns allow-scripts, grants allow-same-origin only to cross-origin feature URLs, and denies everything else unless opted in; see SandboxOptions for the managed tokens and why. Host-decreed and never baked by a shell build. Only meaningful for the iframe modes: opening popup or standalone with a sandbox set throws, because no containment can apply to a top-level window.Properties
◆ Types
Only the mount functions handed in are reachable, so a shell that passes the modes its feature contract declares (as generated shells do) ships no code for the others. Pass builtInDisplayModes to support every mode.
type DisplayModeMap = Partial< Record< DisplayMode, DisplayModeMount>>type DisplayModeMount = ( context: MountContext) => MountResulthealthy: beats are arriving within the expected budget.unobservable: silence carries no information yet. Either a page is
healthy back. suspect: the pages are visible and the miss budget is exhausted; the
gone: the session is closed or destroyed (or not yet open).
type HeartbeatState = "healthy" | "unobservable" | "suspect" | "gone"close (the default) treats the interaction as a close request and starts the polite teardown; event surfaces it as a dismiss event for the host consumer to handle; none ignores it.type BackdropBehavior = "close" | "event" | "none"center (the default) centers on both axes; the compound values anchor to an edge or corner of the area.type BoxPosition = "center" | "top-left" | "top-center" | "top-right" | "center-left" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right"type DismissSource = "backdrop" | "escape"type EventHandler = ( data: unknown) => voidBrowsers deny powerful features (camera, fullscreen, clipboard, …) to cross-origin frames by default, so a feature that needs one only works when the host delegates it. The union lists the common names for editor completion; any string the browser understands is accepted.
type FeaturePermission = "accelerometer" | "autoplay" | "camera" | "clipboard-read" | "clipboard-write" | "display-capture" | "encrypted-media" | "fullscreen" | "gamepad" | "geolocation" | "gyroscope" | "magnetometer" | "microphone" | "midi" | "payment" | "picture-in-picture" | "publickey-credentials-get" | "screen-wake-lock" | "usb" | "web-share" | "xr-spatial-tracking" | string & { }type RequestHandler = ( data: unknown) => unknownnone is the local default (opt-in security); production builds must pick v3 (ephemeral session keys) or v4 (session keys bound to a pre-shared key).type SecurityProtocol = "none" | "v3" | "v4"gone: the frame provably no longer exists: its iframe was taken out of
present: the frame is still there, so the silence is either a stall that
present, because no web API reports that to the embedding page; only time tells the two apart.type UnresponsiveFrame = "present" | "gone"emit (the default) emits an error carrying { reason: 'unresponsive', missedBeats, lastBeatAt, displayMode, frame }; unmount also tears the feature down after emitting the same error; a callback takes over handling entirely with the UnresponsiveInfo. The policy runs once per suspect episode: a recovering beat returns the feature to healthy and re-arms it. Hidden pages and freshly resumed watching both read unobservable: silence is weak evidence until a beat earns healthy. reopen (or a ReopenPolicy to tune it) emits the same error, then brings the feature back if its silence outlasts a grace period: the mount, crash placeholder included, is replaced by a fresh one opened with the same options, and a reopen event carrying { attempt, attempts, displayMode } announces each attempt. Nothing is reopened into a hidden page; the attempt waits for the page to be watched again, then allows a fresh grace. A reopened session that never completes its handshake counts as another death in the same episode. The policy stands down when the frame is gone (the feature is torn down instead, since the page or the visitor removed it), when the host calls close, destroy, or open itself, when the feature closes the session, and when a reopen fails in a way another attempt cannot mend (a refused window, a denied handshake, or a mount that throws).type UnresponsivePolicy = "emit" | "unmount" | "reopen" | ReopenPolicy | ( info: UnresponsiveInfo) => void● Variables
createShell composes a shell from this full map; generated shells import the individual mounts and compose only the modes their feature declared, so this map (and the modes it would drag in) stays out of their bundles.The pane is a single transparent iframe spanning the host viewport; the feature renders its dialog box inside it and the transparent remainder is the backdrop. It mounts hidden (inert to the user and the page) and is revealed once the session opens. Backdrop and in-frame Escape interactions are detected by the feature side and cross as dismiss signals the shell acts on per
dialogBackdrop/closeOnEscape; an Escape pressed while the host document holds focus is handled here directly.By default the iframe fills the container's content box — measured before the iframe is inserted, so the announcement carries the container's own dimensions — and a reporter forwards every later change (with viewport-derived fallback dimensions while the container has none). When the merged options agree a fixed
embedWidth/embedHeight, the iframe receives exactly those dimensions and the host application places the container so the feature fits. The frame mounts hidden and is revealed once the session opens.The window opens at the agreed
popupWidth/popupHeight (falling back to a viewport-derived size), placed on the screen per popupPosition (centered by default). Once open, the window is the browser's: the user may move and resize it freely, the feature's own window is its viewport (no viewport reports cross the boundary), and no sandbox or permissions delegation can apply to a top-level window. The window's title and chrome belong to the loaded document and the browser: the feature sets document.title; the host cannot.The simplest mode: the browser's normal new-tab behavior is sufficient, so no sizing or presentation coordination applies, only the ordinary session lifecycle over the opener relationship.
The host selects the mode; a feature declares which modes it supports in its
feature.config.* display.modes, and the generated shell composes exactly those.