Real-time layout/tiling engine with animated resizing, panel movement, and tab groups for Svelte, inspired by Hyprland.
See PROVENANCE.md for the original revision and the integrated changes.
git clone https://github.com/Eriskii/svelte-pane-engine.git
cd svelte-pane-engine
npm ci
npm run build
npm test
npm packDevelopment requires Node 22+. npm test exercises layout, geometry, tab motion, and DOM
lifecycle using Happy DOM. No desktop or display server is required. npm pack builds/tests and
produces eriskii-svelte-pane-engine-<version>.tgz.
The distributable entry point and declarations are generated in dist/. Consumers import @eriskii/svelte-pane-engine rather than reaching into src or engine DOM.
Releases are published to GitHub Packages. A consuming project maps the @eriskii scope to
that registry in its .npmrc:
@eriskii:registry=https://npm.pkg.github.comand authenticates with a read:packages token in ~/.npmrc
(//npm.pkg.github.com/:_authToken=<token>). Then:
npm install @eriskii/svelte-pane-engineBump version in package.json, commit, and push a matching v<version> tag.
.github/workflows/release.yml tests the tagged commit and publishes it to GitHub Packages.
In ErisMonorepo this repository is checked out at
libs/svelte-pane-engine and linked into every app through npm workspaces, so apps there build against the
local source instead of the published release.
Provide a sized host element and a renderer for your panel content:
import { PaneEngine } from '@eriskii/svelte-pane-engine';
import '@eriskii/svelte-pane-engine/style.css';
const engine = new PaneEngine(document.querySelector<HTMLElement>('#workspace')!, {
createRenderer() {
const element = document.createElement('div');
return {
element,
update(panel, visible) {
element.textContent = panel.title;
element.hidden = !visible;
},
dispose() {
element.remove();
},
};
},
});
engine.addPanel({ id: 'welcome', component: 'text', title: 'Welcome' });
const savedLayout = engine.toJSON();
engine.fromJSON(savedLayout);
// When the host application removes this workspace:
engine.dispose();For Svelte 5 components, pass createSvelteRenderer({ text: YourPanel }) as createRenderer.
Each component receives a readable model store with id, title, params, and visible.
The renderer contract itself is framework-neutral; the package currently declares Svelte 5
as a peer because the main entry also exports the optional Svelte bindings.
| Area | Entry points |
|---|---|
| Engine lifecycle | PaneEngine, PanePanelHandle, addPanel, removePanel, dispose |
| State and persistence | PaneLayoutState, emptyPaneLayout, isPaneLayoutState, toJSON, fromJSON |
| Geometry and movement | Layout/geometry helpers, drag sessions, split resizing and drop targets |
| Host integration | PanePanelRenderer, group extensions, stable group elements, semantic hit testing |
| Svelte adapter | SveltePaneRenderer, createSvelteRenderer, SveltePaneProps |
State snapshots are cloned. The host owns where they are saved and when they are restored.
The engine owns its DOM and renderer lifetimes; dispose registrations and the engine when their
host owners are removed. API declarations are generated alongside the JavaScript in dist/.
PanePanelRenderer.update(panel, visible, context)receives the current group and its stable element handles.groupElements(groupId)exposes the group root, tab bar, tab list, and content container without selector scraping.groupElements(groupId).tabElements(panelId)exposes the tab wrapper, its native activationbutton, and a separateactionsoutlet. Append application buttons to that outlet; their clicks and pointer gestures do not select or drag the tab. The wrapper returned bytabElement(panelId)remains stable across unchanged updates.registerGroupExtension(factory)owns one application extension per physical group view. It updates on every engine synchronization—including transient drag views—and disposes when that physical view is removed. Exiting views remain alive through their exit animation.hitTest(target)converts engine-owned DOM into semantic tab, tab-action, tab-close, panel, group, or split hits.createDropDecoration(options)owns and reliably clears one class-based drop preview.positionForDropTarget(target)commits the same semantic target without a second hit test.PaneEngineOptions.groupMotionOrigin({ group, panels, rect, bounds })supplies a group's opening/closing rectangle, for example just beyond the nearest host edge. Existing groups still reflow from their current geometry; explicit drag origins, immediate restores, and reduced motion take precedence. Omitting the callback retains the default scale/fade. Closing renderers remain mounted until their animation finishes.paneMinimumSize(node, panels, gap)exposes the solver's minimum extent so host resize gestures can respect the same tab and subtree constraints.setGap(gap)changes the space between groups without replacing views;gapreads the current value, so host geometry uses the same spacing as the engine.
The engine owns layout state, geometry, and generic DOM. ErisDE owns its panel catalog and state, Svelte header component, keyboard/pointer policy, header-extra teleport convention, and application styling. In particular, .panel-header-extras and [data-pane-group-drag-id] are application contracts, not pane-engine APIs.
demo.mp4
MIT; see LICENSE.