deskpane

Framework-agnostic web desktop window management engine
2,911 3
Install
npm install deskpane
Latest Version:0.3.3
Vue:3
License:Apache-2.0
Last Updated:Jul 21, 2026
Links: GitHub  ·  npm
Maintainer: 0obriano0

DeskPane

A framework-agnostic web desktop window management framework.
Build floating windows, draggable panels, virtual desktops, and ERP-style layouts directly in the browser — no dependencies, works with Vue, React, or plain JS.

npm version npm downloads license bundle size

Links: Live demos · Developer docs · npm


Why DeskPane?

Modern web applications — especially ERPs, dashboards, and admin tools — often need a desktop-like window management experience. Most solutions are either tied to a specific framework, require a heavy dependency tree, or lack the polish needed for production use.

DeskPane is:

  • 🪶 Zero core dependencies — pure TypeScript core; framework adapters use optional peer dependencies
  • 🔌 Framework-agnostic — works with Vue 3, React 18, jQuery, or plain JS
  • 🎨 Themeable — 30 CSS custom properties, light/dark built-in, fully customizable
  • 📦 Modular — use only what you need (core / desktop / workspace / menu)
  • 🏗️ Production-ready — used in real ERP systems

Contents


Features

Core Window Manager

  • ✅ Open / close / minimize / maximize / restore windows
  • ✅ Drag & resize at 60 fps with configurable throttle
  • ✅ Snap alignment — windows snap to edges and each other while dragging and resizing
  • ✅ Child windows (parentId / modal) — z-order management, cascade close, modal overlay with shake feedback
  • ✅ resizable: false — fixed-size dialog pattern
  • ✅ Focus / z-order management
  • ✅ RWD viewport clamping — windows auto-clamp via ResizeObserver; never open off-screen
  • ✅ Event bus — subscribe to any window lifecycle event
  • ✅ Isolated mode — embed a desktop inside any container element

Desktop Module (deskpane/desktop)

  • ✅ Virtual desktop with draggable icons and localStorage snap positions
  • ✅ Wijmo-style itemsSource — bind desktop icons to arrays or DesktopCollectionView
  • ✅ Optional Dock leading / trailing slots for Start buttons, clocks, and system tray content
  • ✅ Typed SystemTray items with actions, status-only content, badges, custom renderers, and events
  • ✅ Optional Dock itemRenderer and taskbar button layout with keyboard activation
  • ✅ Desktop icon events — items:changed, icon:moved, icon:activated, and more
  • ✅ Dock with frosted-glass backdrop-filter, drag reorder
  • ✅ Windows-style group thumbnail preview — hover Dock item to see live window thumbnails
  • ✅ syncDockWithWindows() — zero-config Dock ↔ window sync

Menu Module (deskpane/menu)

  • ✅ Optional StartMenu and ContextMenu bundle; existing Desktop and Dock APIs stay unchanged
  • ✅ Nested submenus, disabled / checked items, separators, shortcuts, and command callbacks
  • ✅ Keyboard navigation, outside-click close, viewport clamping, and theme variables

Workspace Module (deskpane/workspace)

  • ✅ WorkspaceManager — multiple virtual desktops with slide animation
  • ✅ TaskView — workspace switcher overlay with real DOM-clone thumbnails
  • ✅ SessionManager — serialize / restore window state

Layouts & Theming

  • ✅ BorderLayout — N/S/E/W/Center docking layout, collapsible panels, draggable splitters
  • ✅ Theme system — bundled light/dark themes, CSS custom properties, custom theme names or direct CSS paths, plus legacy medieval-pixel compatibility
  • ✅ Vue 3 adapter — useWindowManager, DpDesktop, DpDesktopIcon, DpWindowManager, DpWindow
  • ✅ React 18 adapter — useWindowManager hook + createPortal support

Installation

npm install deskpane

Import Modes

Use one style-loading strategy per app:

Mode Best for Recommendation
Bundler manual CSS import Vite, Vue, React, app-level theme overrides Import CSS yourself and set injectStyles:false
Runtime CSS injection Small demos, script-tag prototypes Let DeskPane inject core CSS
CDN CSS + UMD jQuery, CMS pages, no build step Use <link> tags and window.DeskPane

Bundler CSS

<!-- Equivalent files are available under dist/styles/ and dist/themes/. -->
import 'deskpane/dist/styles/deskpane.css'
import 'deskpane/dist/styles/deskpane-desktop.css'
import 'deskpane/dist/styles/deskpane-workspace.css'
import 'deskpane/dist/styles/deskpane-taskview.css'
import 'deskpane/dist/themes/light.css'

When CSS is imported by the host app, disable runtime injection:

const wm = new WindowManager({
  container: document.getElementById('desktop')!,
  isolated: true,
  injectStyles: false,
})

CDN / Script Tag

<link rel="stylesheet" href="https://unpkg.com/deskpane/dist/styles/deskpane.css">
<link rel="stylesheet" href="https://unpkg.com/deskpane/dist/themes/light.css">
<script src="https://unpkg.com/deskpane/dist/deskpane.umd.min.js"></script>

jsDelivr works too:

<script src="https://cdn.jsdelivr.net/npm/deskpane/dist/deskpane.umd.min.js"></script>

For the jQuery adapter, load jQuery first, then the adapter bundle:

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://unpkg.com/deskpane/dist/deskpane-jquery.umd.min.js"></script>

Quick Start

ES Module

import 'deskpane/dist/styles/deskpane.css'
import 'deskpane/dist/themes/light.css'
import { WindowManager } from 'deskpane'

const wm = new WindowManager({
  container: document.getElementById('desktop')!,
  isolated: true,
  injectStyles: false,
})

const el = document.createElement('div')
el.innerHTML = '<p style="padding:20px">Hello, World!</p>'

wm.open({ id: 'hello', title: 'My Window', content: el })

Script Tag (UMD)

<div id="desktop" style="width:100vw; height:100vh; position:relative;"></div>
<link rel="stylesheet" href="https://unpkg.com/deskpane/dist/styles/deskpane.css">
<link rel="stylesheet" href="https://unpkg.com/deskpane/dist/themes/light.css">
<script src="https://unpkg.com/deskpane/dist/deskpane.umd.min.js"></script>
<script>
  var wm = new window.DeskPane.WindowManager({
    container: document.getElementById('desktop'),
    isolated: true,
    injectStyles: false
  })
  var el = document.createElement('div')
  el.textContent = 'Hello from UMD!'
  wm.open({ id: 'hello', title: 'My Window', content: el })
</script>

jQuery Adapter

<div id="desktop" style="width:100vw;height:100vh;position:relative"></div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://unpkg.com/deskpane/dist/deskpane-jquery.umd.min.js"></script>
<script>
  $('#desktop').dpWindowManager({
    isolated: true,
    snap: true,
    injectStyles: false
  })

  $('<div class="panel">Hello from jQuery</div>').dpWindow({
    manager: '#desktop',
    id: 'hello-jquery',
    title: 'jQuery Window',
    width: 420,
    height: 260
  })
</script>

The adapter adds:

Plugin Purpose
$(root).dpWindowManager(options) Create and store a WindowManager on a container
$(root).dpWindowManager('open', config) Command-style access to open, close, minimize, maximize, restore, focus, getBodyElement, and getState
$(element).dpWindow({ manager, ...config }) Open the selected jQuery element as a DeskPane window
$(root).dpDesktop(options) Create a Desktop, optional internal WindowManager, Dock, and icons
$(root).dpWorkspaceManager(options) Create and store a WorkspaceManager, optionally tied to a dpDesktop instance
$(element).dpWorkspaceWindow({ workspace, ...config }) Open the selected jQuery element in the current or selected workspace
$(root).dpTaskView(options) Create and control a TaskView overlay, optionally using the desktop Dock

Workspace + TaskView example:

<script>
  $('#desktop').dpDesktop({
    dock: { position: 'bottom', items: [] },
    windowManager: false
  })

  $('#desktop').dpWorkspaceManager({
    desktop: '#desktop',
    workspaces: [
      { id: 'ws-1', label: 'Desktop 1' },
      { id: 'ws-2', label: 'Desktop 2' }
    ],
    syncDock: true,
    windowManagerOptions: {
      isolated: true,
      snap: true,
      injectStyles: false
    }
  })

  $('#desktop').dpTaskView({
    desktop: '#desktop',
    allowAdd: true,
    allowDelete: true
  })

  $('<div>Customers</div>').dpWorkspaceWindow({
    workspace: '#desktop',
    appId: 'customers',
    title: 'Customers'
  })
</script>

Vue 3 — Composable

<template>
  <div ref="desktopEl" class="desktop">
    <button @click="openVueWindow({ id: 'w1', title: 'My Window', component: MyComp })">
      Open Window
    </button>
    <Teleport v-for="win in windows" :key="win.id" :to="win.bodyEl">
      <component :is="win.component" v-bind="win.props ?? {}" />
    </Teleport>
  </div>
</template>

<script setup lang="ts">
import { useWindowManager } from 'deskpane/vue'
import MyComp from './MyComp.vue'

const { windows, openVueWindow } = useWindowManager()
</script>

Vue 3 — Declarative Components

<template>
  <DpDesktop
    v-model:items="icons"
    :dock="{ position: 'bottom' }"
    @initialized="desktop = $event"
    @icon-activated="openApp"
  >
    <DpDesktopIcon
      id="settings"
      label="Settings"
      icon="⚙️"
      @activate="settingsOpen = true"
    />
  </DpDesktop>

  <DpWindowManager>
    <DpWindow
      id="settings"
      v-model:open="settingsOpen"
      title="Settings"
      icon="⚙️"
      :width="520"
      :height="420"
      @initialized="onWindowInitialized"
    >
      <SettingsPanel />
    </DpWindow>
  </DpWindowManager>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { DpDesktop, DpDesktopIcon, DpWindow, DpWindowManager } from 'deskpane/vue'
import type { DesktopIconConfig, DesktopIconEvent } from 'deskpane/desktop'
import SettingsPanel from './SettingsPanel.vue'

const settingsOpen = ref(false)
let desktop: unknown = null
const icons = ref<DesktopIconConfig[]>([
  { id: 'settings-icon', label: 'Settings', icon: '⚙️', x: 40, y: 40 },
])

function openApp(event: DesktopIconEvent) {
  if (event.id === 'settings-icon') settingsOpen.value = true
}

function onWindowInitialized({ bodyEl }: { bodyEl: HTMLElement }) {
  // Attach vanilla JS / jQuery / third-party widgets here.
  console.log(bodyEl)
}
</script>

initialized follows the Wijmo-style pattern: it fires after the underlying DeskPane instance or window body is created, so advanced users can store the instance or attach vanilla JS / jQuery behavior without leaving the declarative template API.

Declarative Vue windows also support core child/modal behavior. Use parent-id to attach a window to its parent and modal to block the parent until the child is closed:

<DpWindow id="settings" v-model:open="settingsOpen" title="Settings">
  <SettingsPanel />
</DpWindow>

<DpWindow
  id="settings-confirm"
  v-model:open="confirmOpen"
  title="Confirm changes"
  parent-id="settings"
  modal
>
  <ConfirmChanges />
</DpWindow>

React 18

import { createPortal } from 'react-dom'
import { useWindowManager } from '@deskpane/adapters/react/useWindowManager'
import MyComp from './MyComp'

export default function App() {
  const { windows, openReactWindow } = useWindowManager()

  return (
    <div className="desktop">
      <button onClick={() => openReactWindow({ id: 'w1', title: 'My Window', component: MyComp })}>
        Open Window
      </button>
      {windows.map(win => {
        const Component = win.component
        return Component
          ? createPortal(<Component {...(win.props ?? {})} />, win.bodyEl, win.id)
          : null
      })}
    </div>
  )
}

API Reference

new WindowManager(options?)

Option Type Default Description
container HTMLElement document.body Desktop container element
isolated boolean false Constrain windows to container (position: absolute)
throttleMs number 16 Drag/resize throttle interval (ms)
snap boolean true Enable snap alignment while dragging and resizing
snapThreshold number 20 Snap trigger distance (px)
snapGap number 0 Gap between windows when snapping (px). Container edges are not affected.

RWD: Automatically monitors container size via ResizeObserver and clamps all windows back into view when the viewport shrinks.

Methods

Method Description
wm.open(config) Open a window (restores & focuses if ID already exists)
wm.close(id) Close and remove a window
wm.minimize(id) Minimize a window
wm.maximize(id) Maximize a window
wm.restore(id) Restore from minimized or maximized
wm.focus(id) Bring window to front
wm.setTitle(id, title) Update the window title bar
wm.setSnapGap(gap) Dynamically update the window-to-window snap gap (px)
wm.shake(id) Trigger shake animation on a window (used for modal blocking feedback)
wm.getChildIds(parentId) Get an array of child window IDs attached to a parent
wm.getRootWindowId(id) Walk up the parent chain to find the root window ID
wm.getState(id) Get current WindowState
wm.getBodyElement(id) Get the window's content HTMLElement
wm.getWindowIds() Get all open window IDs
wm.destroy() Destroy all windows and clean up DOM

WindowConfig

interface WindowConfig {
  id:          string    // Unique window ID (required)
  title:       string    // Window title bar text
  icon?:       string    // Emoji or image URL shown in Dock (default: '🪟')
  label?:      string    // Short Dock label; falls back to title when omitted
  content:     any       // HTMLElement, or null when using framework adapters
  x?:          number    // Initial X position (px)
  y?:          number    // Initial Y position (px)
  width?:      number    // Initial width (px), default 640
  height?:     number    // Initial height (px), default 480
  resizable?:  boolean   // Default true. Set false to disable maximize + border-drag resize
  parentId?:   string    // Attach as a child of this window ID; child stays above parent z-index
  modal?:      boolean   // Requires parentId. true = parent gets an overlay; clicking it shakes child
  props?:      Record<string, unknown>
  slotType?:   'dom' | 'vue' | 'react'
}

Fixed-size Windows

Pass resizable: false to lock a window to its initial size — the maximize button is visually disabled and border-drag resizing is blocked:

wm.open({
  id:        'alert',
  title:     '⚠️ Confirm',
  content:   myDialogEl,
  width:     360,
  height:    200,
  resizable: false,   // maximize button disabled, border drag blocked
})

Child Windows

Use parentId to attach a window as a child. The child's z-index is always above its parent. Children minimize/restore together with their parent and are excluded from the Dock:

// Non-modal child — both windows can be freely focused
wm.open({
  id:       'prop-panel',
  title:    'Properties',
  parentId: 'main-app',
  modal:    false,
  content:  panelEl,
})

// Modal child — parent gets a semi-transparent overlay while child is open
wm.open({
  id:       'confirm-dialog',
  title:    'Confirm',
  parentId: 'main-app',
  modal:    true,       // clicking the overlay shakes the child
  width:    360,
  height:   200,
  resizable: false,
  content:  dialogEl,
})

Events

wm.events.on('window:opened',       (state) => { })
wm.events.on('window:closed',       (state) => { })
wm.events.on('window:focused',      (state) => { })
wm.events.on('window:minimized',    (state) => { })
wm.events.on('window:maximized',    (state) => { })
wm.events.on('window:restored',     (state) => { })
wm.events.on('window:maximized-drag-restored', (state) => { })
wm.events.on('window:drag-start',   (state) => { })
wm.events.on('window:drag-end',     (state) => { })
wm.events.on('window:resize-start', (state) => { })
wm.events.on('window:resize-end',   (state) => { })
wm.events.on('window:edge-snap-preview', ({ id, edgeSnapTarget }) => { })
wm.events.on('window:edge-snap-preview-clear', ({ id }) => { })
wm.events.on('window:edge-snapped', ({ edgeSnapTarget, ...state }) => { })
wm.events.on('window:moved',        (state) => { })
wm.events.on('window:resized',      (state) => { })
wm.events.on('window:child-opened', ({ parentId, childId }) => { })
wm.events.on('window:child-closed', ({ parentId, childId }) => { })

Theming

The general-purpose bundled themes are dist/themes/light.css and dist/themes/dark.css. dist/themes/medieval-pixel.css remains published as a legacy compatibility theme for existing applications.

The Win7 and XP-like themes are showcase CSS owned by demo/win7/win7-theme.css and demo/xp/xp-theme.css. They demonstrate how far DeskPane can be customized, but they are not npm package themes. Copy and adapt them in your application when you want that visual direction.

Structural styles are provided separately as dist/styles/deskpane.css (window structure), dist/styles/deskpane-desktop.css (Desktop / Dock / Icon), dist/styles/deskpane-workspace.css (workspace slide animation), dist/styles/deskpane-taskview.css (TaskView overlay), and the optional dist/styles/deskpane-menu.css (StartMenu / ContextMenu). These are independent of theme variables and can be <link>ed directly:

<link rel="stylesheet" href="dist/styles/deskpane.css">
<link rel="stylesheet" href="dist/styles/deskpane-desktop.css">
<!-- optional: only needed when using WorkspaceManager / TaskView -->
<link rel="stylesheet" href="dist/styles/deskpane-workspace.css">
<link rel="stylesheet" href="dist/styles/deskpane-taskview.css">
<!-- optional: only needed when using StartMenu / ContextMenu -->
<link rel="stylesheet" href="dist/styles/deskpane-menu.css">

DeskPane supports two stable CSS loading modes:

// Auto-inject mode: useful for CDN demos and quick starts.
const desktop = new Desktop()
const wm = new WindowManager()
// Manual import mode: recommended for bundlers and app-level overrides.
import 'deskpane/styles/deskpane.css'
import 'deskpane/styles/deskpane-desktop.css'
import 'deskpane/styles/deskpane-workspace.css'
// Optional:
import 'deskpane/styles/deskpane-menu.css'

const desktop = new Desktop({ injectStyles: false })
const ws = new WorkspaceManager(desktop.getElement(), {
  injectStyles: false,
  windowManagerOptions: { isolated: true, snap: true },
})

When runtime injection is enabled, DeskPane first checks for an existing matching <link> or bundler-created <style> and skips duplicate injection. Runtime styles are inserted before app-level stylesheets in <head>, so project overrides loaded later remain authoritative. WorkspaceManager.injectStyles:false is also propagated to the internally-created WindowManager unless windowManagerOptions.injectStyles is set explicitly.

Alternatively, use getCoreCSS() / getDesktopCSS() / getWorkspaceCSS() / getTaskViewCSS() / getMenuCSS() for programmatic injection:

import { getCoreCSS } from 'deskpane'
import { getDesktopCSS } from 'deskpane/desktop'
import { getWorkspaceCSS, getTaskViewCSS } from 'deskpane/workspace'
import { getMenuCSS } from 'deskpane/menu'
// inject into shadow root, iframe, or custom container

Load a theme

<link id="dp-theme" rel="stylesheet" href="dist/themes/light.css">

setTheme(theme, options?)

import { setTheme } from 'deskpane'

setTheme('dark')                               // resolves to themes/dark.css
setTheme('light', { basePath: '/themes' })     // resolves to /themes/light.css
setTheme('medieval-pixel', { basePath: 'dist/themes' })
setTheme('dark',  { basePath: 'dist/themes' }) // relative path

// Direct .css paths and URLs are used as-is.
setTheme('/DeskPane/demo/win7/win7-theme.css')
setTheme('/DeskPane/demo/xp/xp-theme.css')
setTheme('https://example.com/themes/company.css')

// UMD: DeskPane.setTheme('dark', { basePath: 'dist/themes' })

CSS Custom Properties — Core (15)

:root {
  /* Window chrome */
  --dp-window-border:               #d0d0d0;
  --dp-window-border-active:        #b0b8c8;
  --dp-window-shadow:               0 4px 24px rgba(0,0,0,0.18);
  --dp-window-shadow-active:        0 8px 36px rgba(0,0,0,0.28);
  --dp-window-bg:                   var(--dp-window-body-bg, #ffffff);
  /* Header */
  --dp-window-header-bg:            #f5f5f5;
  --dp-window-header-border:        #e0e0e0;
  --dp-window-title-color:          #333333;
  /* Buttons */
  --dp-window-btn-color:            #555555;
  --dp-window-btn-hover-bg:         #e0e0e0;
  --dp-window-btn-close-hover-bg:   #ff5f57;
  --dp-window-btn-close-hover-color:#ffffff;
  /* Body */
  --dp-window-body-bg:              #ffffff;
  --dp-window-body-color:           #222222;
  /* Snap guide */
  --dp-snap-guide-color:            rgba(0,120,255,0.55);
}

CSS Custom Properties — Layout module (7)

:root {
  --dp-layout-header-bg:       #ebebeb;
  --dp-layout-header-border:   #d8d8d8;
  --dp-layout-title-color:     #444444;
  --dp-layout-btn-color:       #666666;
  --dp-layout-btn-hover-bg:    #d8e4f0;
  --dp-layout-splitter-bg:     #d0d0d0;
  --dp-layout-splitter-active: #b0b8c8;
}

CSS Custom Properties — Desktop module (8)

:root {
  --dp-desktop-bg:             linear-gradient(135deg,#e8eaf0 0%,#d0d4e0 100%);
  --dp-desktop-icon-text:      #1a1a2e;
  --dp-desktop-icon-hover-bg:  rgba(0,0,0,0.08);
  --dp-dock-bg:                rgba(220,225,240,0.20);  /* semi-transparent for frosted glass */
  --dp-dock-backdrop-filter:   blur(4px);               /* frosted-glass blur behind dock */
  --dp-dock-border:            rgba(0,0,0,0.12);
  --dp-dock-item-hover-bg:     rgba(0,0,0,0.08);
  --dp-font:                   system-ui,-apple-system,sans-serif;
}

Set --dp-dock-backdrop-filter: none to disable the blur effect entirely.


Desktop Module

import { Desktop } from 'deskpane/desktop'
import { WindowManager } from 'deskpane'

const desktop = new Desktop({
  container: document.getElementById('root')!,
  dock: { position: 'bottom', items: [] },
  iconSnap: true,
  dragThreshold: 6,
})

const wm = new WindowManager({
  container: desktop.getElement(),
  isolated: true,
})

desktop.addIcon({ id: 'notepad', label: '📝 Notepad', icon: '📝', action: () => {
  wm.open({
    id:    'notepad',
    title: '📝 Notepad',   // shown in window title bar
    label: 'Notepad',      // short label shown in Dock (falls back to title if omitted)
    icon:  '📝',           // icon shown in Dock
    content: document.createElement('div'),
  })
}})

// Sync running windows ↔ Dock items automatically (zero config)
const stopSync = desktop.syncDockWithWindows(wm)
// Later: stopSync() to detach

// Dynamically change Dock position at runtime
desktop.setDockPosition('top')    // 'top' | 'bottom' | 'left' | 'right'
// Icon area and window area insets update instantly

// Get the full desktop root element (includes Dock)
desktop.getDesktopElement()

DesktopConfig

Option Type Default Description
container HTMLElement document.body Container for the desktop
dragThreshold number 6 Global drag start threshold (px)
iconSnap boolean true Enable icon snap alignment
iconSnapThreshold number 20 Icon snap trigger distance (px)
storageKey string 'dp-desktop' localStorage key prefix for icon positions
dock DockConfig {} Dock configuration
icons DesktopIconConfig[] [] Initial desktop icons
itemsSource DesktopIconConfig[] | DesktopCollectionView icons ?? [] Wijmo-style data source for desktop icons

Dock leading and trailing slots

DockConfig.leading and DockConfig.trailing accept either a DOM Node or a renderer. Use them for shell chrome that should stay outside the scrollable running-window items, such as a Start button, clock, or system tray.

SystemTray is an optional helper for the trailing slot. You can still pass any custom Node directly, so existing Dock integrations keep the same behavior.

import { Desktop, SystemTray } from 'deskpane/desktop'

const startButton = document.createElement('button')
startButton.className = 'start-button'
startButton.textContent = 'Start'

const tray = new SystemTray({
  items: [
    {
      id: 'network',
      label: 'Network',
      icon: '📶',
      badge: 2,
      action: () => openNetworkSettings(),
    },
    {
      id: 'clock',
      label: 'Current time',
      interactive: false,
      renderer: ({ container }) => {
        const clock = document.createElement('time')
        clock.textContent = new Date().toLocaleTimeString()
        container.appendChild(clock)
      },
    },
  ],
})

const desktop = new Desktop({
  container: document.getElementById('root')!,
  dock: {
    position: 'bottom',
    items: [],
    leading: startButton,
    trailing: ({ position, container }) => {
      container.dataset.position = position
      return tray.getElement()
    },
  },
})

With either slot enabled, Dock renders three stable regions:

[.dp-dock-leading] [.dp-dock-items........] [.dp-dock-trailing]

The center .dp-dock-items region owns scrolling, so leading and trailing controls remain visible. For left and right Docks, the same regions flow from top to bottom. When neither slot is configured, DeskPane preserves the legacy DOM and keeps every .dp-dock-item as a direct Dock child.

A renderer runs again whenever Dock rebuilds or changes position. Return a fresh Node when appropriate, or pass a persistent Node directly. Do not share one Node between both slots.

Dock item renderer and taskbar layout

SystemTray

SystemTray manages compact status and command items without taking over Dock layout. Items with an action render as native buttons; items without an action default to status-only semantics. Set interactive explicitly when you need to override that behavior.

const tray = new SystemTray({
  ariaLabel: 'System status',
  items: [
    { id: 'volume', label: 'Volume', icon: '🔊', action: openSoundSettings },
    { id: 'updates', label: 'Updates', icon: '⬆', badge: 3, action: openUpdates },
  ],
})

desktop.getDock().setTrailing(tray.getElement())

tray.events.on('tray:item-activated', ({ id }) => {
  console.log('activated', id)
})

tray.events.on('tray:item-contextmenu', ({ id, originalEvent }) => {
  originalEvent.preventDefault()
  openTrayMenu(id, originalEvent.clientX, originalEvent.clientY)
})

tray.setBadge('updates', null)

The default presentation accepts the same URL, inline SVG, emoji/text, or DOM Node icon forms as desktop icons. Use an item renderer or the tray-level itemRenderer for clocks, meters, Web Components, or other custom content. A supplied Node is moved into its item; do not share one Node between multiple items.

SystemTray methods
Method Description
tray.setItems(items) Replace all tray items
tray.addItem(item, index?) Add an item at an optional clamped index
tray.updateItem(id, patch) Patch an item and re-render; returns whether it existed
tray.removeItem(id) Remove an item; returns whether it existed
tray.setBadge(id, badge) Set a string/number badge, or pass null to clear it
tray.setDisabled(id, disabled) Toggle item activation
tray.setItemRenderer(renderer) Replace the tray-level fallback renderer
tray.refresh() Re-run item renderers without changing data
tray.getItems() / tray.getItem(id) Read copied item data
tray.getItemElement(id) Get the managed item host
tray.getElement() Get the root Node for a Dock slot
tray.destroy() Clear listeners and remove the tray root

Events are available from tray.events: tray:item-activated, tray:item-contextmenu, and tray:items-changed. A listener can call originalEvent.preventDefault() during tray:item-activated to cancel the configured action. DockConfig.itemLayout defaults to 'dock', preserving the existing icon-first appearance and DOM. Opt into 'taskbar' for horizontal top/bottom window buttons. itemRenderer customizes only the content inside each managed .dp-dock-item; DeskPane continues to own activation, drag reorder, active classes, keyboard handling, and window previews.

const desktop = new Desktop({
  container: document.getElementById('root')!,
  dock: {
    position: 'bottom',
    itemLayout: 'taskbar',
    itemRenderer: ({ item, container, renderDefault }) => {
      renderDefault() // append the built-in icon and label

      const running = document.createElement('span')
      running.textContent = '●'
      running.title = `${item.label} is running`
      running.style.color = '#4ade80'
      container.dataset.appId = item.id
      return running
    },
  },
})

The renderer can return a Node or append directly to container. Call renderDefault() once when you want to decorate the standard content; omit it when replacing the content completely. Keep activation handlers on DockItemConfig.action so mouse, Enter, Space, previews, and drag behavior stay consistent.

Taskbar dimensions can be themed without replacing structural CSS:

.my-desktop {
  --dp-taskbar-item-width: 152px;
  --dp-taskbar-item-height: 34px;
  --dp-taskbar-item-gap: 7px;
  --dp-taskbar-item-padding: 3px 8px;
  --dp-taskbar-item-radius: 4px;
  --dp-taskbar-item-justify: flex-start;
}

taskbar changes horizontal top/bottom items only. Left/right Docks retain their compact vertical arrangement. Existing projects that do not set itemLayout or itemRenderer keep their prior appearance.

Dock Methods

Method Description
dock.setItemLayout('dock' | 'taskbar') Switch the built-in item arrangement dynamically
dock.getItemLayout() Get the current item arrangement
dock.setItemRenderer(renderer) Replace the item content renderer; pass null to restore built-in content
dock.setSlot('leading' | 'trailing', content) Replace one slot dynamically; pass null to clear it
dock.setLeading(content) Replace or clear the leading slot
dock.setTrailing(content) Replace or clear the trailing slot
dock.getSlotElement(name) Get the current slot host, or null in legacy mode
dock.getItemsElement() Get the scrollable center strip; returns the Dock root in legacy mode

Desktop icon content and custom rendering

DesktopIconConfig.icon keeps the existing URL, inline SVG, and emoji behavior, and also accepts a real DOM Node. Use iconRenderer when each refresh should create fresh content or when rendering Canvas, Web Components, or eventful HTML.

const statusIcon = document.createElement('span')
statusIcon.className = 'status-icon'
statusIcon.textContent = '42'

desktop.addIcon({
  id: 'status',
  label: 'Status',
  icon: statusIcon, // DesktopIcon takes ownership of this Node
  action: openStatus,
})

desktop.addIcon({
  id: 'chart',
  label: 'Live chart',
  iconRenderer: ({ item, container }) => {
    container.dataset.appId = item.id

    const canvas = document.createElement('canvas')
    canvas.width = 48
    canvas.height = 48
    drawChart(canvas)
    return canvas
  },
  action: openChart,
})

Do not share one Node between multiple icons. Use iconRenderer to create one Node per rendered icon. A renderer may return a string or Node, or append directly to container and return nothing; when both are provided, iconRenderer takes priority over icon.

Desktop Methods

Method Description
desktop.addIcon(config) Add a desktop icon
desktop.removeIcon(id) Remove a desktop icon
desktop.getItems() Get the current desktop icon snapshot, including live positions
desktop.getItem(id) Get one icon snapshot
desktop.setItems(items) Replace the desktop icon list
desktop.setItemsSource(source) Bind a new array or DesktopCollectionView
desktop.updateItem(id, patch) Patch one icon and refresh its rendered element
desktop.refreshItems() / desktop.refresh() Re-read the bound source and repaint desktop icons
desktop.getCollectionView() Get the active DesktopCollectionView
desktop.getDock() Get the Dock instance for advanced manipulation
desktop.setDockPosition(pos) Dynamically move Dock: 'top' | 'bottom' | 'left' | 'right'. Updates icon and window area insets instantly.
desktop.getElement() Get the window area element (Dock excluded) — use as WindowManager container
desktop.getDesktopElement() Get the full desktop root element (includes Dock)
desktop.syncDockWithWindows(wm, opts?) Sync Dock items with running windows
desktop.unsyncDockWithWindows() Stop sync
desktop.destroy() Tear down the desktop

DesktopCollectionView

import { Desktop, DesktopCollectionView } from 'deskpane/desktop'

const icons = [
  { id: 'erp', label: 'ERP', icon: '📦', x: 40, y: 40 },
]

const view = new DesktopCollectionView(icons)
const desktop = new Desktop({ itemsSource: view })

view.add({ id: 'mail', label: 'Mail', icon: '✉️', x: 40, y: 140 })
view.update('erp', { label: 'ERP System' })
view.refresh() // use this after direct sourceCollection mutations

desktop.events.on('items:changed', event => {
  console.log(event.reason, event.items)
})
desktop.events.on('desktop:ready', event => {})
desktop.events.on('desktop:destroyed', event => {})
desktop.events.on('items:refreshed', event => {})
desktop.events.on('icon:added', event => {})
desktop.events.on('icon:removed', event => {})
desktop.events.on('icon:moved', event => {})
desktop.events.on('icon:activated', event => {})
desktop.events.on('icon:selected', event => {})
desktop.events.on('dock:position-changed', event => {})

desktop.syncDockWithWindows(manager, options?)

Option Type Default Description
getAppIdFromWindowId (windowId) => string | null strip app- prefix Map window ID → app ID. Return null to skip.
getDockItem (appId, event) => { label, icon } | null event.label ?? event.title + event.icon ?? 🪟 Build Dock display. Return null to skip.
onDockItemClick (appId, windowId) => void focus window Custom click handler
dedupeByAppId boolean true One Dock item per app ID
syncExisting boolean true Sync already-open windows at bind time
showWindowPreview boolean true Enable Windows-style group thumbnail preview on hover
previewSize { width: number; height: number } { width: 160, height: 100 } Thumbnail card size per window (aspect-ratio preserved)
previewMountEl HTMLElement auto-detect .v-application Element to mount the preview popup into. Auto-detects the nearest .v-application ancestor so Vuetify / Vue scoped CSS / CSS variables are inherited by cloned thumbnails. Pass your app root if the detection fails (e.g., document.getElementById('app')).

Dock Methods

Method Description
dock.addItem(item) Append a DockItem to the end
dock.addItemAt(item, index) Insert at position index (0 = leftmost)
dock.removeItem(id) Remove item by id
dock.onRender(cb) Subscribe to DOM-rebuild events (fires after every _render()). Returns an offRender function. Useful for re-binding hover/event listeners after Dock reorders.

Group thumbnail preview — Hovering a Dock item for 280ms shows a Windows-style card strip: one card per window (parent + all children). Each card has a title and a × close button (appears on hover). The popup is sticky — mouse can move into it without it disappearing. Modal safety: clicking × on a parent card while a modal child exists shakes the child instead of closing. Thumbnails are automatically re-bound after drag-reorder or new window opens via Dock.onRender.

Vuetify / Scoped CSS — The popup is auto-mounted inside the nearest .v-application ancestor (or previewMountEl) so cloned thumbnails correctly inherit Vuetify selectors, Vue data-v-* scoped styles, and CSS custom properties. Positioning always uses position:fixed with viewport coordinates, unaffected by the mount point.

Demo — demo/desktop/index.html ships a full virtual desktop experience with Dock, draggable icons, theme switching, snap-gap control, live Dock position switching (top/bottom/left/right), a 📐 BorderLayout demo window (Basic + Nested tabs), and a child window / modal dialog demo (System Settings window).

Theme Demo — demo/desktop/index.html is the main desktop demo, featuring the Medieval Pixel theme, Dock, draggable icons, virtual desktops, and 9-slice panel borders.

GitHub Pages — run npm run build:pages to build the library, Vue demo, React demo, docs, and the static Pages artifact in .pages/. Vue/React/docs demos are served from their generated dist/ folders on Pages. The CI Pages deployment runs from version tags (v*), and GitHub Releases also include a downloadable deskpane-pages-vX.X.X.zip demo bundle.


Menu Module

deskpane/menu is an independent, opt-in bundle. Importing it does not change existing WindowManager, Desktop, or Dock behavior.

import { ContextMenu, StartMenu } from 'deskpane/menu'
import 'deskpane/styles/deskpane-menu.css'

const startMenu = new StartMenu({
  anchor: document.getElementById('start-button')!,
  injectStyles: false,
  header: { label: 'My Desktop', icon: 'DP' },
  items: [
    { id: 'orders', label: 'Orders', icon: '📦', action: openOrders },
    {
      id: 'settings',
      label: 'Settings',
      children: [
        { id: 'theme', label: 'Theme', action: openThemeSettings },
      ],
    },
  ],
  secondaryItems: [
    { id: 'help', label: 'Help', action: openHelp },
  ],
  footerItems: [
    { id: 'sign-out', label: 'Sign out', action: signOut },
  ],
})

const contextMenu = new ContextMenu({
  injectStyles: false,
  items: [
    { id: 'open', label: 'Open', action: openSelected },
    { id: 'refresh', label: 'Refresh', shortcut: 'F5', action: refreshDesktop },
    { type: 'separator' },
    { id: 'properties', label: 'Properties', action: openProperties },
  ],
})

// Prevent the browser menu and open DeskPane at the pointer position.
const unbind = contextMenu.bindTo(document.getElementById('desktop')!)

// Lifecycle cleanup:
unbind()
startMenu.destroy()
contextMenu.destroy()

Both menu classes support URL, data URL, inline SVG, and emoji icons. Submenus automatically flip near viewport edges. Arrow keys, Home, End, Enter, and Escape work without extra wiring.

Menu Methods

Class Methods
ContextMenu showAt(x, y, items?), showFor(anchor, placement?, offset?, items?), bindTo(element, getItems?), setItems(items), hide(), isOpen(), getElement(), destroy()
StartMenu open(), close(), toggle(), setItems(items), setSecondaryItems(items), setFooterItems(items), setHeader(header), isOpen(), getElement(), destroy()

Menu Events

menu.events.on('menu:open',  ({ source, element }) => {})
menu.events.on('menu:close', ({ reason, element }) => {})
menu.events.on('menu:select', ({ id, item, originalEvent }) => {})

For script tags, load dist/deskpane-menu.umd.min.js and use window.DeskPaneMenu. Runtime style injection is enabled by default; manual CSS users should pass injectStyles:false.


Workspace Module

WorkspaceManager — Multiple Virtual Desktops

import { WorkspaceManager } from 'deskpane/workspace'

const wsMgr = new WorkspaceManager(desktop.getElement(), {
  animationMs: 220,                            // slide animation duration (default 250ms)
  windowManagerOptions: { isolated: true, snap: true },
})

wsMgr.addWorkspace({ id: 'ws-1', label: 'Desktop 1' })
wsMgr.addWorkspace({ id: 'ws-2', label: 'Desktop 2' })
wsMgr.switchTo('ws-2')

// Get the WindowManager for the active workspace
const wm = wsMgr.getWindowManager(wsMgr.current.id)
wm.open({ id: 'app', title: 'My App', content: el })

// Safer: let DeskPane scope the window id to the workspace.
// This prevents duplicate raw ids when the same app opens on multiple desktops.
wsMgr.openWindow({
  appId: 'counter',
  title: 'Counter',
  icon: '🔢',
  label: 'Counter',
  content: el,
})
// Actual window id becomes similar to: ws-2::app-counter

// Events
wsMgr.events.on('workspace:switched', ({ from, to }) => { })
wsMgr.events.on('workspace:added',    (state) => { })
wsMgr.events.on('workspace:removed',  ({ id }) => { })

// Optional dot indicator (shown inside the workspace root)
wsMgr.enableIndicator()
WorkspaceManagerOptions
Option Type Default Description
animationMs number 250 Slide animation duration (ms). Set 0 for instant switch.
injectStyles boolean true Auto-inject workspace CSS
windowManagerOptions WindowManagerOptions {} Options passed to every workspace's WindowManager
WorkspaceManager Methods
Method Description
addWorkspace(config) Create a new workspace (auto-activates if first)
removeWorkspace(id) Destroy workspace and switch to nearest remaining
switchTo(id) Animate to target workspace
getWindowManager(id) Get the WindowManager for a workspace
openWindow(config) Open a window in a workspace. Prefer appId to generate a workspace-scoped id automatically.
createWindowId(appId, workspaceId?) Build a workspace-scoped id such as ws-2::app-counter.
enableIndicator() Show dot indicator below workspaces
disableIndicator() Remove dot indicator
destroy() Destroy all workspaces and clean up
Workspace-safe window ids

When the same app can open on more than one workspace, avoid reusing a raw id like app-counter in every workspace. Use openWindow({ appId }) or the helper functions exported from deskpane/workspace:

import {
  createWorkspaceWindowId,
  getAppIdFromWorkspaceWindowId,
  parseWorkspaceWindowId,
} from 'deskpane/workspace'

const id = createWorkspaceWindowId('ws-2', 'counter') // ws-2::app-counter
parseWorkspaceWindowId(id)                            // { workspaceId:'ws-2', appId:'counter' }
getAppIdFromWorkspaceWindowId(id)                     // counter

WorkspaceManager warns by default when the same raw window id is opened in multiple workspaces because it can confuse Dock sync and framework Portal/Teleport targets. Disable this only if you intentionally manage ids yourself:

const wsMgr = new WorkspaceManager(container, {
  warnOnDuplicateWindowIds: false,
})
React / Vue Portal Content with Workspaces

DeskPane owns the window DOM, but framework-rendered content inserted with React createPortal or Vue <Teleport> remains application state. When using WorkspaceManager, keep that state scoped to the active workspace and resync it whenever the active workspace changes.

This matters when the same app/window id can exist in more than one workspace. A key based only on window.id can cause React or Vue to reuse the wrong portal target after switching desktops, which may appear as an empty or black window body until the window is recreated.

Recommended integration pattern:

type FrameworkWindowEntry = {
  workspaceId: string
  id: string
  bodyEl: HTMLElement
  component: any
}

let windows: FrameworkWindowEntry[] = []
const disposers: Array<() => void> = []

function syncWindows() {
  const current = wsMgr.current
  if (!current) return

  const wm = wsMgr.getWindowManager(current.id)
  windows = wm.getWindowIds().map(id => ({
    workspaceId: current.id,
    id,
    bodyEl: wm.getBodyElement(id)!,
    component: resolveComponent(id),
  }))
}

function subscribeWorkspace(workspaceId: string) {
  const wm = wsMgr.getWindowManager(workspaceId)
  disposers.push(
    wm.events.on('window:opened', syncWindows),
    wm.events.on('window:closed', syncWindows),
    wm.events.on('window:restored', syncWindows),
  )
}

disposers.push(
  wsMgr.events.on('workspace:added', state => subscribeWorkspace(state.id)),
  wsMgr.events.on('workspace:switched', syncWindows),
)

// On app unmount:
disposers.forEach(dispose => dispose())

Use a workspace-aware key for portal/teleport nodes:

// React
windows.map(win => {
  const Component = win.component
  return createPortal(<Component />, win.bodyEl, `${win.workspaceId}:${win.id}`)
})
<!-- Vue -->
<Teleport v-for="win in windows" :key="`${win.workspaceId}:${win.id}`" :to="win.bodyEl">
  <component :is="win.component" />
</Teleport>

TaskView — Workspace Switcher Overlay

Shows a full-screen overlay with real DOM-clone thumbnails of every workspace. Clicking a card switches to that workspace.

import { TaskView } from 'deskpane/workspace'

const taskView = new TaskView(wsMgr, {
  dock:        desktop.getDock(),  // auto-insert toggle button at leftmost Dock position
  showButton:  true,               // set false to suppress auto-button; open() still works
  buttonLabel: '虛擬桌面',          // label shown in the Dock button
  buttonIcon:  '⧉',                // icon shown in the Dock button
  allowAdd:    true,               // show "Add Desktop" button
  allowDelete: true,               // show delete button on cards
  keyboard:    true,               // Escape closes the overlay
})

// Programmatic open/close (always works even if showButton: false)
taskView.open()
taskView.toggle()

// Events
taskView.events.on('taskview:open',  () => { })
taskView.events.on('taskview:close', () => { })

// Cleanup
taskView.destroy()
TaskViewOptions
Option Type Default Description
target HTMLElement document.body Where to mount the overlay
dock DockLike — When provided, auto-inserts a toggle button at position 0 (leftmost)
showButton boolean true Insert toggle button into Dock. Set false to hide while keeping open() callable.
buttonLabel string '虛擬桌面' Label for the auto-managed Dock button
buttonIcon string '⧉' Icon for the auto-managed Dock button
buttonId string 'taskview-btn' ID for the auto-managed Dock button
allowAdd boolean true Show "New Desktop" button
allowDelete boolean true Show delete button on workspace cards
keyboard boolean true Close on Escape key
closeOnBackdrop boolean true Close when clicking the overlay backdrop
onCreateWorkspace () => WorkspaceConfig auto ws-N / 桌面 N Custom workspace config for new workspaces
injectStyles boolean true Auto-inject TaskView CSS

Developer Documentation

An in-depth interactive docs site is included at demo/docs/ (Vue 3 SPA, i18n EN / zh-TW).

cd demo/docs && npm install && npm run dev   # http://localhost:3002

Pages covered (21 total):

Category Pages
Getting Started Overview, Installation, Quick Start, Troubleshooting
Core API WindowManager Options, Open & Close, Min / Max / Restore, Snap & Alignment, Events, Session
Theming Theme System
Desktop Module Desktop & Dock, BorderLayout
Workspace Module Workspace, TaskView
Vanilla JS Hello World, DOM Content, jQuery
Vue 3 useWindowManager, KeepAlive
React useWindowManager

Troubleshooting

CSS import vs runtime injection

If your app imports DeskPane CSS through a bundler, keep style order predictable by disabling runtime injection:

import 'deskpane/dist/styles/deskpane.css'
import 'deskpane/dist/styles/deskpane-desktop.css'
import 'deskpane/dist/themes/light.css'

new Desktop({
  container,
  injectStyles: false,
})

new WorkspaceManager(desktop.getElement(), {
  injectStyles: false,
  windowManagerOptions: {
    isolated: true,
    snap: true,
    injectStyles: false,
  },
})

Vite source-development CSS error

When developing directly against DeskPane source, Vite may report:

deskpane-desktop.css does not provide an export named default

DeskPane's library build converts internal CSS imports into strings. Vite dev needs the demo raw CSS plugin so DeskPane source CSS can be imported as a string while the host app's manual CSS imports still use normal CSS handling.

Vue / React workspace content goes black

DeskPane owns the window DOM. Vue and React content is mounted by Teleport / createPortal. In multi-workspace apps:

  • Make real window ids workspace-scoped, for example ws-2::counter.
  • Include workspaceId:id in portal / teleport keys.
  • Re-sync framework state after workspace:switched.

Transparent window or pointer-event issues

Core CSS should give .dp-window, .dp-header, and .dp-body reliable backgrounds and pointer-events:auto. If your app overrides these classes, check that runtime CSS is not injected after your override stylesheet.


BorderLayout

Embed an EasyUI-style docking layout inside any window. wm.open() auto-detects data-region children and renders the layout.

<script>
const content = document.createElement('div')
content.innerHTML = `
  <div data-region="north" data-title="Toolbar" data-icon="🔧" data-size="40" data-collapsible></div>
  <div data-region="west"  data-title="Nav"     data-icon="📁" data-size="200" data-collapsible>
    <p>Sidebar</p>
  </div>
  <div data-region="center"><p>Main content</p></div>
  <div data-region="east"  data-title="Props"   data-icon="🔍" data-size="180" data-collapsible></div>
  <div data-region="south" data-title="Status"  data-size="28" data-collapsible></div>
`
wm.open({ id: 'app', title: 'My App', width: 900, height: 600, content })
</script>

data-* attributes

Attribute Description
data-region="north|south|east|west|center" Region direction
data-size="200" Width (E/W) or height (N/S) in px
data-min-size="60" Minimum drag size in px
data-collapsible Allow collapsing (presence flag)
data-collapsed Initially collapsed
data-title="Label" Show region header bar
data-icon="🔧" Icon shown before title

When collapsed, a region shrinks to a 28px mini strip: expand button → icon → rotated title.


Vue 3 Adapter

useWindowManager(opts?) returns:

Return Type Description
wm WindowManager Underlying instance
windows ShallowRef<VueWindowEntry[]> Reactive list for v-for + <Teleport>
openVueWindow(config) fn Open a Vue component window
close / minimize / maximize / restore / focus / setTitle fn Proxy methods

React 18 Adapter

useWindowManager(opts?) returns:

Return Type Description
wm WindowManager Underlying instance
windows ReactWindowEntry[] State array for createPortal mapping
openReactWindow(config) fn Open a React component window
close / minimize / maximize / restore / focus / setTitle fn Proxy methods

Build Output

File Format Size Use when
dist/deskpane.es.js ESM ~23 KB Vite / Webpack / type="module" (dev)
dist/deskpane.es.min.js ESM ~12 KB Production ESM
dist/deskpane.umd.js UMD ~26 KB Script tag / jQuery (dev)
dist/deskpane.umd.min.js UMD ~12 KB Production CDN
dist/deskpane-desktop.es.js / .min.js ESM — Desktop module (ESM)
dist/deskpane-desktop.umd.js / .min.js UMD — Desktop module (window.DeskPaneDesktop)
dist/deskpane-workspace.es.js / .min.js ESM — Workspace + TaskView + Session module (ESM)
dist/deskpane-workspace.umd.js / .min.js UMD — Workspace module (window.DeskPaneWorkspace)
dist/deskpane-menu.es.js / .min.js ESM — Optional StartMenu + ContextMenu module (import 'deskpane/menu')
dist/deskpane-menu.umd.js / .min.js UMD — Optional Menu module (window.DeskPaneMenu)
dist/deskpane-jquery.es.js / .min.js ESM — jQuery adapter (import 'deskpane/jquery')
dist/deskpane-jquery.umd.js / .min.js UMD — jQuery adapter (window.DeskPaneJQuery, auto-installs into window.jQuery)
dist/index.d.ts TypeScript — Core type declarations
dist/desktop.d.ts TypeScript — Desktop type declarations
dist/workspace.d.ts TypeScript — Workspace + TaskView + Session type declarations
dist/menu.d.ts TypeScript — StartMenu + ContextMenu type declarations
dist/jquery.d.ts TypeScript — jQuery adapter type declarations
dist/themes/light.css CSS ~2 KB Light theme (Core + Desktop)
dist/themes/dark.css CSS ~2 KB Dark theme (Core + Desktop)
dist/themes/medieval-pixel.css CSS ~3 KB Legacy compatibility theme (Core + Desktop + pixel UI assets)
dist/themes/assets/medieval-pixel/ Assets — Pixel UI source assets used by the Medieval Pixel theme
dist/styles/deskpane.css CSS — Core window structure styles (direct <link>)
dist/styles/deskpane-desktop.css CSS — Desktop / Dock / Icon styles (direct <link>)
dist/styles/deskpane-layout.css CSS — BorderLayout / Panel styles (direct <link>)
dist/styles/deskpane-workspace.css CSS — Workspace container / slide animation styles (direct <link>)
dist/styles/deskpane-taskview.css CSS — TaskView overlay / card / thumbnail styles (direct <link>)
dist/styles/deskpane-menu.css CSS — Optional StartMenu / ContextMenu styles (direct <link>)

Building from Source

npm install

npm run build        # Type-check only (tsc --noEmit, no JS output)
npm run build:lib    # Build all bundles → dist/
npm run clean        # Clean dist/
npm run release      # clean + build:lib + package release/

Requires Node.js 18+. Library build uses Rollup.


Browser Support

Any modern browser supporting ES2020 (optional chaining, nullish coalescing, Map, Set).


Roadmap

  • CDN publish (jsDelivr / unpkg auto-sync after npm publish)
  • Angular adapter
  • Window state persistence (localStorage / IndexedDB)
  • Accessibility (ARIA roles, keyboard navigation)
  • Theme customization and showcase themes

Contributing

Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request.

Useful checks:

npm run build
npm run build:lib
npm --prefix demo/vue run build
npm --prefix demo/react run build
npm --prefix demo/docs run build

Community Standards


License

Apache-2.0 © 2026 Brian Cheng

See LICENSE for full terms.

Related Packages

@surdeddd/wmkit

Headless, framework-agnostic window manager for the web — draggable, resizable,...

5,018 4
ai-chat-embed

Framework-agnostic embed wrapper for ai-suspended-ball-chat

1,933 3
@ozankurt/context-menu

Framework agnostic right click context menu with a zero dependency TypeScript co...

0 2
@korneevecin/vue3-dialogs-lib

Window manager for Vue 3: minimizable, draggable, persistable windows on the nat...

836 0
viz

Visual regression testing framework

5,559 0

Version History

Version Released Vue License
0.3.3 >=3.4.0 Apache-2.0
0.3.2 >=3.4.0 Apache-2.0
0.3.1 >=3.4.0 Apache-2.0
0.3.0 >=3.4.0 Apache-2.0
0.2.3 >=3.4.0 Apache-2.0