Skip to content

Repository files navigation

@eriskii/svelte-pane-engine

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.

Build and test

git clone https://github.com/Eriskii/svelte-pane-engine.git
cd svelte-pane-engine
npm ci
npm run build
npm test
npm pack

Development 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.

Install

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.com

and authenticates with a read:packages token in ~/.npmrc (//npm.pkg.github.com/:_authToken=<token>). Then:

npm install @eriskii/svelte-pane-engine

Release

Bump 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.

Minimal browser usage

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.

Public API

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/.

Integration contracts

  • 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 activation button, and a separate actions outlet. Append application buttons to that outlet; their clicks and pointer gestures do not select or drag the tab. The wrapper returned by tabElement(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; gap reads 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.

Upstream demo

demo.mp4

License

MIT; see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages