Pane and window lifecycle
A single owner, multiple views
A pane registry maps type strings to synchronous mount functions that return a view object or nothing:
(context: PaneContext) => {
dispose(): void;
ready?: Promise<void>; // optional framework commit acknowledgement
resize?(width: number, height: number): void;
update?(pane: Pane): void;
}context contains element, document, window, pane, state, reportError, and location (main or popout). Use the supplied document/window for DOM creation, timers, event listeners, canvas contexts, portals, and computed styles. The element may initially be detached; use resize or ResizeObserver for measurements.
Register view-owned resources with context.onCleanup(callback) as you allocate them. Quilt runs these callbacks in reverse order even when mounting fails before returning. context.signal is aborted before cleanup on view teardown. Late cleanup registrations run immediately. Returned dispose() is still supported; exceptions are reported without preventing other cleanup, replacement, or window closure. Handle asynchronous data loading inside the view with a loading/error state; returning a Promise is not supported.
In the main document, stable pane IDs retain their view across resizing, tab activation, movement, and maximize/restore. Inactive tabs stay mounted and hidden. Tab switches and maximize/restore preserve vertical and horizontal scroll positions in pane content, including nested scroll areas. Offsets are restored after the retained view is visible and sized; newly mounted views still own their initial scroll state. Changes to type or params remount that pane. Hiding or moving a view within its document does not abort its signal. Metadata changes call update when provided. Data changes should flow through application-owned subscriptions, not repeated layout JSON updates.
Popout transaction
- A user action calls
mounted.popout(id). The call opens the window synchronously and returnsPromise<boolean>for completion. - The browser opens a blank same-origin window. Blocking leaves the original pane untouched and emits an error.
- The library copies head stylesheet elements and basic document theme attributes, then runs
prepareWindow. - The destination view mounts against the same application-owned state. It briefly coexists with the old view; avoid exclusive singleton resources during mounting.
- If supplied, the destination view’s
readypromise must resolve. Rejection, closure, or disposal cancels the attempt. Only successful mounting commits the detached placement and disposes the old view. A failed mount closes the destination and preserves the original placement/view. - A Return action or detected window close restores the pane. The old group is preferred; if constraints no longer permit it, a compatible group or new region is used. Returning also exits maximize mode so the pane is discoverable.
Popouts contain one pane. They cannot form separate split/tab workspaces in v1. Use Return before moving a popped-out pane elsewhere.
Application-owned data must be updated continuously. An input value or React useState living only inside the old view will not survive remounting. Map engines should place document/history state outside the renderer and explicitly release graphics resources. A simulated job belongs to the application's job owner, not a pane's lifetime.
Browser behavior
Window opening requires a user gesture. Requested size/position can be adjusted by the browser and OS; some environments may open a tab. Placement JSON stores requested geometry, not continuous tracking of OS window movement. Access to multiple monitors is not requested.
Loading JSON never attempts unsolicited window creation. Detached records without live window handles dock into the main layout. Popouts are session-only: exportWorkspace() saves a docked copy and retains no reopening intent. Raw core exports still describe live state.
Quilt owns the mounted workspace store. Vanilla callers dispose the workspace once; React unmount handles disposal automatically. Supplied stores are rejected. The main window owns the session. Explicit disposal closes companions, releases mounts, and marks accessible orphan documents disconnected. Page departure attempts the same teardown. Close polling supplements pagehide detection. Browser crashes, mobile termination, or lost processes do not guarantee unload callbacks; this library is not a durable state store.
The default companion starts at about:blank and inherits the opener origin. A custom window factory must return a same-origin document synchronously. Cross-origin navigation is unsupported and triggers return/cleanup when access is lost. Do not request noopener: this architecture intentionally requires an opener-owned session.
Styles and React providers
Linked styles and inline style elements are copied into companions and refreshed when the source head changes. Document/body class, style, and data attributes are synchronized. Ancestor-specific selectors, shadow-root styles, adopted stylesheets, and assets initialized by scripts need application integration through prepareWindow or pane wrappers.
Declarative React panes use portals and inherit their application providers, including in companion documents. Callback and component-map identity changes update options without rebuilding the workspace. Initial configuration is mount-only; changing the React key replaces the session. Standalone reactRenderer still creates an independent root and needs explicit provider wrappers. Document transitions still remount content; React-local state does not travel.
Close confirmation is opt-in through pane confirmClose or a unified registration default. Quilt supplies a dialog; confirmClose(request) can replace it with an application-owned asynchronous dialog. Capability close: false still prevents user closes. Explicit core commands bypass confirmation. Closing a popout returns the pane rather than discarding its application data.
Corner gestures and pending content
Corner dragging previews the new region using the same constraint allocation as the renderer. The dominant drag axis chooses split orientation. A gesture across regions outlines the rectangle spanning the starting region and the region under the pointer. The starting region survives as the receiver, keeping its active tab visible. This works across rows and columns, including perpendicular splits. All regions within that rectangle must be fully contained; partially intersected regions, disallowed joins, and arrangements that cannot remain a binary split tree show an invalid preview. Dragging farther expands the selection, and dragging back shrinks it; the cursor path does not accumulate regions. The affected split tree is restructured without moving surrounding boundaries. Core callers can supply complete node bounds through joinRegions(receiverId, otherId, { rectangles }); joinRectangle exposes the same pure planning operation. Without rectangles, the existing same-axis join behavior and optional extents remain available. Joining retains all selected content as tabs in the receiver, preserving its ID, active tab, and tab settings. Measured extents retain surrounding sizes, subject to content constraints. Every selected pane must permit joining. A layout change during a gesture cancels it. Escape and pointer cancellation remove the preview without changing state.
Releasing a split creates an empty group before opening a centered registry chooser. Empty regions can also be split repeatedly, even when the entire layout has no content. Without a registry, splitting an empty region simply creates another empty region. Choosing content adds it to that stable group ID. With the default autoCollapse: "disabled", clicking outside the empty region dismisses its chooser without removing the region. Click the diagonal empty surface to reopen search. Escape releases search focus. Multiple pending regions can coexist; select content later or use the empty tab bar’s “Close empty pane” button. With "enabled" or "protected", cancelling removes only the still-empty new group, preserving unrelated edits. Empty groups are valid JSON and provide a chooser if restored after a reload. Applications can use workspace.splitGroup(groupId, axis, null, options) and workspace.removeEmptyGroup(groupId) for the same workflow; filled groups are never removed by the latter.
Automatic collapse
Use workspace.setAutoCollapse('disabled') on a mounted workspace, or supply a saved setting through initialWorkspace. Standalone core users can configure new LayoutStore(layout, { autoCollapse: 'disabled' }). AutoCollapse accepts 'enabled' | 'protected' | 'disabled'; omitted means 'disabled'. Read or change it with workspace.getAutoCollapse() and workspace.setAutoCollapse(mode). This is a workspace-wide session setting shared by DOM and React. Raw core layout JSON excludes it, and core load and reset preserve it. Workspace presets include it; loadWorkspace applies the saved setting. The demo Theming pane includes a live selector.
enabled: closing, moving, or popping out the last tab removes its empty source region.protected: closing or moving the last tab removes the region; popping it out preserves it.disabled: all three operations preserve the empty region, its ID, and its placement.
Only the region emptied by an action is eligible for automatic removal. Switching modes never prunes existing empty regions. To retain the previous automatic behavior, opt into enabled. Empty tab bars provide “Close empty pane”; the final root cannot be removed. Removing a region never deletes its detached content. Returning popouts prefer the preserved original region, or use the existing fallback placement if it was explicitly removed.
Restoring closed tabs
workspace.restoreClosedPane() restores the latest closed tab, preferring its original region and index. If the region was removed, it uses the same compatible-region fallback as returning a popout. It preserves subsequent layout edits and the tab's ID and metadata. workspace.canRestoreClosedPane() reports whether a tab is available. History retains up to 50 closed tabs for the store session, is excluded from JSON, and clears on successful load/reset or disposal. Failed closes add no history. Closing still disposes the view; restoration mounts it again. Application data remains application-owned and must be retained outside view lifetimes.
Pane actions group Close active tab and Close pane in the Close submenu. Restore closed tab remains in the main actions menu. Both demos bind R to restore; the renderer enables this with shortcuts: true or shortcuts: { restoreClosedTab: true }. Typing, modifier combinations, held keys, and picker or confirmation dialogs do not trigger the shortcut. Actions menus support shortcuts and close when an available shortcut executes.
Close pane closes all docked tabs in the region and removes the region, independently of automatic-collapse settings. workspace.closeGroup(groupId, options) performs this atomically, checking permissions and confirmation unless { force: true } is explicitly supplied. The last region remains empty so the workspace can accept new tabs. Closed tabs remain individually restorable; detached companion tabs are not closed.
Registering view cleanup
const render: PaneRenderer = ({ element, document: doc, signal, onCleanup }) => {
const input = doc.createElement('input');
input.addEventListener('input', () => saveDraft(input.value), { signal });
element.append(input);
const editor = createEditor(element);
onCleanup(() => editor.destroy());
// No return value is required for a simple view.
};Cleanup runs once per mounted view, including failed companion attempts. Keep application data and application-wide jobs outside this scope. React components should still use ordinary effect cleanup for effect-owned resources; the context scope lasts for the whole pane view, not each React effect execution.
Scrollbar resources
Scrollbar controls belong to each pane view and use the existing content node as their native viewport. Settings updates preserve the node, its children, and scroll offsets. A library-owned host contains the controls outside application content. Nested application scroll areas register explicitly through context.registerScrollArea({ host, viewport }); React pane props expose the same callback. Call the returned cleanup before removing a nested host. Pane disposal also removes all registrations, controls, listeners, observers, and timers. Companion views receive current preferences at mount and on live updates.
Application readiness barriers
context.deferReady(promise) registers an additional readiness barrier during mounting or a React layout effect. Quilt first waits for the returned view's ready acknowledgement, then for registered barriers. Rejection prevents a popout commit and preserves the source view. Cleanup still runs if the destination closes before readiness. Register only initialization work here; ongoing jobs remain application-owned and must not block pane readiness indefinitely.
Resize snapping
Interactive divider drags snap to neighboring parallel dividers within 8 CSS pixels by default. An accent-colored guide highlights the aligned edge while snapped. Dragging away releases it; size constraints take priority. Escape restores the original sizes and clears the guide.
Set resizeSnapping: false in vanilla or React workspace options, or call workspace.updateOptions({ resizeSnapping: false }) to disable it. Workspace presets retain the setting; older presets default to enabled. Both demos expose the setting under Theming → Resizing → Resize snapping.
Core users can supply new LayoutStore(layout, { resizeSnapping: false }), change it with setResizeSnapping, and inspect it with getResizeSnapping. store.snapResize(position, edges, threshold = 8) resolves coordinates without browser dependencies. Hosts supply measurements in a common coordinate space. Direct resize commands and keyboard resize steps remain exact.
Dedicated regions
Pin active tab closes inactive docked tabs and reserves the region for its active view. The operation preserves the region's geometry and IDs and does not remount the retained view. Inactive tabs follow existing close permissions and confirmation rules; cancellation, denied permission, or changed membership or active tab during confirmation leaves the layout unchanged. Closed views enter normal restore history; their application data remains application-owned.
Pinned regions hide the tab bar and its actions button, reserving no content clearance for them. Right-click a corner split/join handle to open pane actions. Explicit contextMenu settings still control right-click availability. Unpin tab releases the restriction without reopening closed tabs. This is distinct from header: false, which hides a single-tab header without restricting the group's contents. Releasing dedication restores ordinary header rules and the region's saved tab styling.
Adding tabs and center drops into pinned regions are rejected, including direct core API commands. Joins are allowed and preserve all views. A pinned receiver becomes tabbed when it receives other views; joining only empty regions preserves its pin. Ordinary join permissions still apply. Splitting alongside a pinned region remains available. Moving or closing the designated view clears dedication on its emptied region. Popping it out reserves a retained source region for its return; normal auto-collapse still applies. If the source region is removed, the existing return fallback applies. Restoring other closed tabs skips dedicated regions and creates an ordinary region if no compatible target exists.