Adapters
Every adapter is a thin wrapper over the same manager and the same DOM controller. They exist to attach and detach at the right moment in a component lifecycle, nothing more. Anything you can do in one, you can do in all of them, and you can always drop to the vanilla API.
The shared contract:
- one manager per desktop, created once and destroyed with the owning component
- your markup carries
data-wm-drag,data-wm-title,data-wm-contentand the control buttons - binding is order-independent: bind a window element before or after the desktop exists, before or after
wm.open() - render a window only while it exists: when its window closes, the framework removes the node it rendered, and renders it again when undo brings the window back. Leave
removeOnCloseto nodes you create yourself (why) - render windows from
state.windows, notstate.order:orderis the stacking order and changes on every focus, so a keyed list over it moves DOM nodes — the click that follows the press is lost and iframes reload. The keys ofstate.windowskeep opening order as long as ids are not bare integers (why), and stacking is applied withz-index, so the DOM order never matters
Vanilla
import { createWindowManager, attachDesktop } from '@surdeddd/wmkit'
import '@surdeddd/wmkit/themes/glass.css'
const wm = createWindowManager()
const desktop = attachDesktop(wm, document.querySelector('#desktop'))
function spawn(init, render) {
const win = wm.open(init)
const el = document.createElement('section')
el.innerHTML = `
<header data-wm-drag>
<span data-wm-title>${win.title}</span>
<span data-wm-controls>
<button data-wm-minimize aria-label="Minimize"></button>
<button data-wm-maximize aria-label="Maximize"></button>
<button data-wm-close aria-label="Close"></button>
</span>
</header>
<div data-wm-content></div>
`
render(el.querySelector('[data-wm-content]'))
document.querySelector('#desktop').append(el)
desktop.attachWindow(win.id, el, { removeOnClose: true })
return win
}
spawn({ title: 'Notes', width: 420, height: 280 }, (body) => {
body.textContent = 'Anything you want.'
})removeOnClose detaches the controller and removes the element when the window closes, so you never leak nodes. Through a binder (what the adapters use), a window that comes back — undo, a reopen, a close and open in one batch — gets its element appended to the desktop element again before it is attached. Call desktop.destroy() when the whole desktop goes away.
React
@surdeddd/wmkit/react
import {
useWindowManager, useDesktop, useWmState, useWmWindow, useWmWindowRef,
} from '@surdeddd/wmkit/react'
import '@surdeddd/wmkit/themes/glass.css'
export function Desktop() {
const wm = useWindowManager({ historyLimit: 100 })
const { ref, binder } = useDesktop(wm, { magnetism: { threshold: 12 } })
const state = useWmState(wm)
return (
<div ref={ref} style={{ position: 'relative', height: '100vh' }}>
<button type="button" onClick={() => wm.open({ title: 'New window' })}>
open
</button>
{Object.keys(state.windows).map((id) => (
<Win key={id} binder={binder} id={id} />
))}
<Taskbar wm={wm} />
</div>
)
}
function Win({ binder, id }) {
const win = useWmWindow(binder.wm, id)
const ref = useWmWindowRef(binder, id)
if (!win) return null
return (
<section ref={ref}>
<header data-wm-drag>
<span data-wm-title>{win.title}</span>
<span data-wm-controls>
<button data-wm-minimize aria-label="Minimize" />
<button data-wm-maximize aria-label="Maximize" />
<button data-wm-close aria-label="Close" />
</span>
</header>
<div data-wm-content>Your React tree lives here — no portals, no innerHTML.</div>
</section>
)
}
function Taskbar({ wm }) {
const state = useWmState(wm)
const minimized = Object.values(state.windows).filter((win) => win.stage === 'minimized')
return (
<div role="toolbar" aria-label="Taskbar">
{minimized.map((win) => (
<button key={win.id} type="button" onClick={() => wm.focus(win.id)}>
{win.title}
</button>
))}
</div>
)
}| Hook | Returns |
|---|---|
useWindowManager(options?) | a manager created once, destroyed on the next macrotask after a real unmount; StrictMode's simulated unmount and Fast Refresh do not destroy it, so listeners added during render or in initializers survive in dev |
useWmState(wm) | the whole ManagerState through useSyncExternalStore |
useWmWindow(wm, id) | one WindowState | undefined, re-rendering only when that window changes |
useDesktop(wm, options?) | { ref, binder, controller() } — put ref on the desktop element |
useWmWindowRef(binder, id, options?) | a ref callback for a window element; options is Omit<WindowAttachOptions, 'removeOnClose'> |
Notes:
useWmWindowis the memo-friendly subscription: unchanged windows keep referential identity, so a list of windows re-renders only the one that moved.optionsforuseDesktopanduseWmWindowRefare captured on first render (they configure a controller that already exists). Change behaviour through the manager, not by swapping options.- React owns the window node, so the adapter never lets the controller remove it: render the window conditionally (
if (!win) return null, as above) and React removes it.removeOnCloseis not an option ofuseWmWindowRef. - StrictMode double-mounting is safe: the adapter tears nothing down in an effect. The desktop ref's
nullcall unbinds, the binder re-subscribes when the ref re-attaches, and the manager's destroy is deferred and cancelled by a remount. Works on React 18 and 19.
Vue
@surdeddd/wmkit/vue
<script setup lang="ts">
import { ref } from 'vue'
import {
useWindowManager, useDesktop, useWmState, useWmWindow, useWmWindowEl,
} from '@surdeddd/wmkit/vue'
import '@surdeddd/wmkit/themes/glass.css'
const host = ref<HTMLElement>()
const wm = useWindowManager()
const binder = useDesktop(wm, host)
const state = useWmState(wm)
const notes = ref<HTMLElement>()
useWmWindowEl(binder, 'notes', notes)
const note = useWmWindow(wm, 'notes')
wm.open({ id: 'notes', title: 'Notes', width: 420, height: 280 })
</script>
<template>
<div ref="host" style="position: relative; height: 100vh">
<section v-if="note" ref="notes">
<header data-wm-drag>
<span data-wm-title>{{ note.title }}</span>
<span data-wm-controls>
<button data-wm-minimize aria-label="Свернуть" />
<button data-wm-maximize aria-label="Развернуть" />
<button data-wm-close aria-label="Закрыть" />
</span>
</header>
<div data-wm-content>{{ state.order.length }} windows open</div>
</section>
</div>
</template>| Composable | Returns |
|---|---|
useWindowManager(options?) | manager, destroyed with the effect scope |
useWmState(wm) | ComputedRef<ManagerState>, read-only (writes are rejected) |
useWmWindow(wm, id) | ComputedRef<WindowState | undefined>; id may be a ref or getter |
useDesktop(wm, targetRef, options?) | DesktopBinder, bound with flush: 'post' |
useWmWindowEl(binder, id, targetRef, options?) | binds a window element, rebinding when the id changes |
v-if on the window removes the section when the window closes and renders it again when it comes back; useWmWindowEl follows the template ref, so the new node is bound on its own. Everything hooks into the current effect scope, so a component that unmounts cleans up. Outside a scope (a module-level manager, for example) nothing is auto-disposed — call wm.destroy() and binder.destroy() yourself. During server rendering useWmState and useWmWindow do not subscribe at all and return live getter refs, so a module-level manager shared across SSR requests does not accumulate listeners.
Svelte
@surdeddd/wmkit/svelte
<script lang="ts">
import { createManager, createDesktop, wmStore, wmWindowStore } from '@surdeddd/wmkit/svelte'
import '@surdeddd/wmkit/themes/glass.css'
const wm = createManager()
const dk = createDesktop(wm)
const state = wmStore(wm)
wm.open({ id: 'notes', title: 'Notes', width: 420, height: 280 })
const notes = wmWindowStore(wm, 'notes')
</script>
<div use:dk.desktop style="position: relative; height: 100vh">
{#if $notes}
<section use:dk.window={{ id: 'notes' }}>
<header data-wm-drag>
<span data-wm-title>{$notes.title}</span>
<span data-wm-controls>
<button data-wm-minimize aria-label="Minimize"></button>
<button data-wm-maximize aria-label="Maximize"></button>
<button data-wm-close aria-label="Close"></button>
</span>
</header>
<div data-wm-content>{$state.order.length} windows open</div>
</section>
{/if}
</div>| Export | Returns |
|---|---|
createManager(options?) | a plain manager — destroy it yourself if the component owns it |
wmStore(wm) | readable store of ManagerState |
wmWindowStore(wm, id) | readable store of one window, emitting only when that window changes |
createDesktop(wm, options?) | { binder, desktop, window } actions |
The window action only rebinds when params.id actually changes, so re-rendering during a drag does not interrupt it. The desktop action releases only its own element when that element goes away: the window bindings stay and attach to the next desktop element, so an {#if} or {#key} around the desktop does not lose them. dk.binder.destroy() drops everything.
Solid
@surdeddd/wmkit/solid
import { For } from 'solid-js'
import { useWindowManager, createDesktop, useWmState, useWmWindow } from '@surdeddd/wmkit/solid'
import '@surdeddd/wmkit/themes/glass.css'
export function Desktop() {
const wm = useWindowManager()
const dk = createDesktop(wm)
const state = useWmState(wm)
wm.open({ title: 'Notes', width: 420, height: 280 })
return (
<div ref={dk.desktop} style={{ position: 'relative', height: '100vh' }}>
<For each={Object.keys(state().windows)}>
{(id) => {
const win = useWmWindow(wm, id)
return (
<section ref={dk.window(id)}>
<header data-wm-drag>
<span data-wm-title>{win()?.title}</span>
<span data-wm-controls>
<button data-wm-minimize aria-label="Minimize" />
<button data-wm-maximize aria-label="Maximize" />
<button data-wm-close aria-label="Close" />
</span>
</header>
<div data-wm-content>fine-grained, obviously</div>
</section>
)
}}
</For>
</div>
)
}useWmWindow accepts a string or an accessor. Everything registers onCleanup when an owner exists, so components dispose themselves; called outside an owner, nothing is auto-disposed.
Inside an owner, dk.desktop and dk.window bind on mount, after Solid inserts the element, so a newly opened window takes focus and the position: relative fix applies; outside an owner they bind immediately. Inside an owner useWmWindow is a memo that notifies only when its window changes, and an accessor id must be reactive (a signal or a prop) to be followed; outside an owner it is a plain live accessor, and on the server it reads live.
Angular
@surdeddd/wmkit/angular
import {
Component, DestroyRef, ElementRef, afterNextRender, inject, input, viewChild,
} from '@angular/core'
import {
type AngularDesktop, createDesktop, useWindowManager, useWmState, useWmWindow,
} from '@surdeddd/wmkit/angular'
@Component({
selector: 'app-notes',
standalone: true,
template: `
<section #panel>
<header data-wm-drag>
<span data-wm-title>Notes</span>
<span data-wm-controls>
<button data-wm-minimize aria-label="Minimize"></button>
<button data-wm-maximize aria-label="Maximize"></button>
<button data-wm-close aria-label="Close"></button>
</span>
</header>
<div data-wm-content><ng-content /></div>
</section>
`,
})
export class NotesComponent {
dk = input.required<AngularDesktop>()
private panel = viewChild.required<ElementRef<HTMLElement>>('panel')
private destroyRef = inject(DestroyRef)
constructor() {
afterNextRender(() => {
this.dk().window('notes', undefined, this.destroyRef)(this.panel().nativeElement)
})
}
}
@Component({
selector: 'app-desktop',
standalone: true,
imports: [NotesComponent],
template: `
<div class="desktop" #host style="position: relative; height: 100vh">
@if (note()) {
<app-notes [dk]="dk">{{ state().order.length }} windows open</app-notes>
}
</div>
`,
})
export class DesktopComponent {
private host = viewChild.required<ElementRef<HTMLElement>>('host')
wm = useWindowManager()
state = useWmState(this.wm)
note = useWmWindow(this.wm, 'notes')
dk = createDesktop(this.wm)
constructor() {
afterNextRender(() => {
this.dk.desktop(this.host().nativeElement)
this.wm.open({ id: 'notes', title: 'Notes', width: 420, height: 280 })
})
}
}| Export | Returns |
|---|---|
useWindowManager(options?) | a manager, destroyed with the component |
useWmState(wm) | a read-only Signal<ManagerState> |
useWmWindow(wm, id) | a computed Signal<WindowState | undefined> |
createDesktop(wm, options?) | { binder, desktop(element, lifetime?), window(id, options?, lifetime?) }; the binder is destroyed with the component |
WmLifetime | type: DestroyRef | Injector, what a binding releases with |
useWindowManager, useWmState, useWmWindow and createDesktop belong in an injection context — a constructor or a field initializer — and release with that component. Called outside one they still work, but nothing releases them: useWmState stays subscribed for as long as the manager lives.
dk.desktop(element, lifetime?) and dk.window(id, options?, lifetime?)(element) bind an element and return a function that releases the binding; calling it twice is harmless. Without a lifetime the adapter uses the injection context of the call, so dk.window('notes') called in a constructor or a field initializer releases with that component even when the function it returns binds the element later, in afterNextRender. Outside an injection context and without a lifetime, call the returned function yourself, or rely on the binder going away with the component that called createDesktop. Binding an id again releases an earlier binding whose element has left the document, so a window component rendered again simply binds its new element. Bind elements in afterNextRender — the view children do not exist before that.
@if (note()) renders the window component only while the window exists: when the window closes, Angular destroys the component and its DestroyRef releases the binding; when undo brings the window back, the component renders and binds again.
useWmState and useWmWindow return real signals, so they work in templates and in computed(). The adapter writes its signal untracked, so a manager change made inside effect() or computed() does not throw NG0600 (Angular 16–18) or leave the signal stale.
Preact
@surdeddd/wmkit/preact
import { h } from 'preact'
import {
useWindowManager, useDesktop, useWmState, useWmWindowRef,
} from '@surdeddd/wmkit/preact'
import '@surdeddd/wmkit/themes/glass.css'
function Win({ binder, win }) {
const ref = useWmWindowRef(binder, win.id)
return h('section', { ref },
h('header', { 'data-wm-drag': '' },
h('span', { 'data-wm-title': '' }, win.title),
h('span', { 'data-wm-controls': '' },
h('button', { 'data-wm-minimize': '', 'aria-label': 'Minimize' }),
h('button', { 'data-wm-maximize': '', 'aria-label': 'Maximize' }),
h('button', { 'data-wm-close': '', 'aria-label': 'Close' }),
),
),
h('div', { 'data-wm-content': '' }, 'Preact content'),
)
}
export function Desktop() {
const wm = useWindowManager()
const { ref, binder } = useDesktop(wm)
const state = useWmState(wm)
return h('div', { ref, style: { position: 'relative', height: '100vh' } },
h('button', { type: 'button', onClick: () => wm.open({ title: 'New window' }) }, 'open'),
Object.values(state.windows).map((win) => h(Win, { key: win.id, binder, win })),
)
}The same five hooks with the same contracts as the React adapter, built on preact/hooks (Preact 10 and later, JSX works the same way). Preact has no useSyncExternalStore, so the hooks subscribe in a layout effect and re-read the snapshot right after subscribing: a change made between render and subscription is not lost.
Lit
@surdeddd/wmkit/lit
import { createWindowManager, createDesktopBinder } from '@surdeddd/wmkit'
import { WmStateController, wmDesktop, wmWindow } from '@surdeddd/wmkit/lit'
import { html, LitElement } from 'lit'
import { repeat } from 'lit/directives/repeat.js'
import '@surdeddd/wmkit/themes/glass.css'
const wm = createWindowManager()
const binder = createDesktopBinder(wm)
class AppDesktop extends LitElement {
private view = new WmStateController(this, wm)
createRenderRoot() {
return this
}
render() {
const windows = Object.values(this.view.state.windows)
return html`<div ${wmDesktop(binder)} style="position: relative; height: 100vh">
${repeat(windows, (win) => win.id, (win) => html`
<section ${wmWindow(binder, win.id)}>
<header data-wm-drag>
<span data-wm-title>${win.title}</span>
<span data-wm-controls>
<button data-wm-minimize aria-label="Minimize"></button>
<button data-wm-maximize aria-label="Maximize"></button>
<button data-wm-close aria-label="Close"></button>
</span>
</header>
<div data-wm-content>Lit content</div>
</section>`)}
</div>`
}
}
customElements.define('app-desktop', AppDesktop)| Export | What it does |
|---|---|
WmStateController(host, wm) | reactive controller; .state is the ManagerState, the host re-renders on every change while connected |
WmWindowController(host, wm, id) | .window is one WindowState | undefined; the host re-renders only when that window changes |
wmDesktop(binder) | element directive that binds the desktop element |
wmWindow(binder, id, options?) | element directive that binds a window element |
The directives bind when the part renders, release when it disconnects and bind again on reconnect, so moving the host around the page is safe. The controllers re-read the state on reconnect, so a host that was detached while the manager changed renders fresh. Key the window list with repeat, so a template instance stays with its window. Render into the light DOM (createRenderRoot() { return this }) for a theme stylesheet in the page to reach the windows. Inside the element's own shadow root, add the theme to its styles instead: static styles = unsafeCSS(themeStyle('glass')), with themeStyle from @surdeddd/wmkit/themes (Theming).
Qwik
@surdeddd/wmkit/qwik
import { component$ } from '@builder.io/qwik'
import { WmDesktop, WmWindow, useWm, useWmState } from '@surdeddd/wmkit/qwik'
import '@surdeddd/wmkit/themes/glass.css'
export const Count = component$(() => {
const state = useWmState()
return <output>{state.value?.order.length ?? 0} windows</output>
})
export const Opener = component$(() => {
const store = useWm()
return <button onClick$={() => store.wm?.open({ title: 'New window' })}>open</button>
})
export default component$(() => (
<WmDesktop class="desktop">
<WmWindow id="notes" title="Notes" x={20} y={60} width={320} height={220}>
<button q:slot="controls" data-wm-close aria-label="Close">×</button>
<p>Server-rendered content, resumable.</p>
</WmWindow>
<Count />
<Opener />
</WmDesktop>
))| Export | What it does |
|---|---|
WmDesktop | renders the desktop element; the manager is created on the client in useVisibleTask$ and provided through WmContext. Props: options (desktop and manager options, data only), options$ (a QRL that returns more options, merged over options on the client), class |
WmWindow | renders a window section: a titlebar with data-wm-title, a controls slot and a content slot. Props: id plus any WindowInit field, class |
useWm() | the desktop store: wm and binder (both undefined until the desktop is visible on the client) and version, bumped on every change |
useWmState() | a computed signal with the ManagerState, undefined on the server |
options is serialized into the server HTML, so it can hold data only. Anything with a function in it goes into options$, which runs on the client: <WmDesktop options={{ snap: true }} options$={() => ({ beforeClose: (w) => w.id !== 'notes' })}>.
WmWindow renders its initial bounds inline, so the server markup sits where the client will put it and resuming does not shift the layout. That inline style is only the starting position; once the window is attached, the controller owns it. The window opens and attaches once the desktop exists on the client. The geometry props stay live: when x, y, width or height changes, the window moves or resizes through the manager, and only the coordinate that changed is applied. The manager's rules still hold, so a position change applies only in the normal stage and a size change is ignored while the window is minimized or maximized. Qwik owns the node: a window closed through the manager is hidden, and it comes back attached when its id is opened again. Unmounting a WmWindow closes its window.
The entry ships as qwik.qwik.mjs, so the Qwik optimizer processes it like your own components. Peer range: @builder.io/qwik 1.5 and later, below 2. A test renders the desktop on the server with no DOM and resumes it with the real Qwik loader in jsdom; it does not cover a real browser, streaming or Qwik City. Qwik City and other server-rendered setups load the Qwik loader for you. With a client-only render() you have to load it yourself, or no onClick$ handler runs:
import { render } from '@builder.io/qwik'
import loader from '@builder.io/qwik/qwikloader.js?raw'
render(document.getElementById('app')!, <App />).then(() => {
const script = document.createElement('script')
script.textContent = loader
document.head.append(script)
})Web Components
@surdeddd/wmkit/elements
<script type="module">
import '@surdeddd/wmkit/elements/define'
import '@surdeddd/wmkit/themes/glass.css'
</script>
<wm-desktop persist="my-desktop" style="display: block; height: 100vh">
<wm-window window-id="notes" label="Notes" x="20" y="20" width="320" height="220">
<textarea>hello</textarea>
</wm-window>
<wm-window window-id="chart" label="Chart" x="360" y="40" width="320" height="220">
<button slot="actions" type="button">↻</button>
<p>chart</p>
</wm-window>
<wm-window window-id="later" label="Later" closed>
<p>opens when the closed attribute goes away</p>
</wm-window>
</wm-desktop>@surdeddd/wmkit/elements/define registers <wm-desktop> and <wm-window>. On a server (Node, any SSR framework) both entries can be imported safely and do nothing. For another prefix, call defineWmElements({ prefix: 'acme' }) from @surdeddd/wmkit/elements to get <acme-desktop> and <acme-window>. The elements work in any framework that renders HTML: React 19, Vue, Angular, Svelte, Solid or plain HTML.
<wm-desktop>
| Attribute or property | Meaning |
|---|---|
snap, keyboard, grouping, magnetism, announce | on by default; ="false" turns one off |
persist="key" | saves the layout to localStorage under the key and restores it |
lang | ru… picks the Russian labels; the nearest lang on the page counts |
.options | any desktop or manager option; set it before the element connects |
.messages | your own labels ({ minimize, maximize, close, tabs? }); wins over lang |
.styles | a CSS string, a CSSStyleSheet or an array of either, adopted into the shadow root of every <wm-window> of this desktop, windows added later included; null removes it |
.manager, .controller | the manager and the controller, once connected |
desktop.styles = retroShadowTheme (or themeStyle('retro', { shadow: true }), both from @surdeddd/wmkit/themes) brings a theme's titlebar, buttons and content box into the chrome of every window, not only its tokens. Where constructable stylesheets are missing (Safari before 16.4), strings are skipped silently. .options, .messages, .styles and .windowId can be assigned before the elements are defined; the values survive the upgrade.
<wm-window>
| Attribute | Meaning |
|---|---|
window-id | the window id; generated when missing, and readable as .windowId |
label | the title |
x, y, width, height, min-width, min-height, max-width, max-height, aspect-ratio, workspace | geometry and constraints |
stage | normal, minimized or maximized; reflects the current stage back |
layer | normal, floating or modal |
closable, minimizable, maximizable, resizable, draggable, snappable | on by default; ="false" turns one off and hides its button |
closed | the window is closed and hidden; removing the attribute reopens it |
chrome="none" | hides the built-in titlebar, for your own data-wm-drag markup |
Changing an attribute drives the manager. Changing one of width / height, x / y, min-* or max-* changes only that side, and the other keeps its current value. Removing x, y, min-width or min-height leaves the window as it is; removing max-width or max-height lifts that limit.
The element mirrors the manager on every change: stage and closed follow undo, redo, hydrate and loadLayout too. Undo after closing a <wm-window> brings the element back, and it fires wm-open; redo closes it again. The element is never removed by the library, so the framework that rendered it keeps ownership. Removing the element from the page closes its window; moving it does not. A second element with the same window-id stays unbound instead of throwing.
A window the user closes stays closed, and with persist and a window-id it stays closed across reloads too, as long as the page keeps rendering an element for it. The ids of closed elements live in wm.getState().extensions.elements ({ closed: ['log'] }), which is serialized and part of history but adds no undo step of its own.
- An element that replaces a closed one under the same
window-id— a re-render, a route transition, a reload withpersist— stays closed too. - Only when the page removes the closed element and keeps it out of the page does its "closed" mark go away; a
<wm-window>rendered for that id later, or after a reload, opens its window. That is how an application that drops the element onwm-closereopens the window: by rendering the element again. - Removing the whole
<wm-desktop>and adding it again with the samepersistkey, as on a route change, keeps closed windows closed. - A
closedattribute in the markup keeps an element closed, and removing it opens the window. When the restored state already holds the window, the element connects open: saved state wins over markup. - Undo after the page has removed a closed element brings the window back in the manager with no element to show it. If your application drops elements on close, render the element again, or listen to the manager.
- An element without
window-idcannot stay closed across reloads, because a generated id is not stable.
The chrome lives in a shadow root with the parts titlebar, title, tabs, tab (plus active on the shown tab), control (plus minimize, maximize, close) and content. The buttons sit in span[data-wm-controls] and show only what the window allows: a button is hidden while minimizable, maximizable or closable is false, so a docked panel has no minimize button. Their glyphs come from CSS ::after, so a theme can replace them. The content box carries data-wm-content as well as part="content". The children stay in the light DOM, in the default slot, and the actions slot puts extra buttons into the titlebar. Style the parts with ::part() (wm-window::part(tab active) { … }) and the theme tokens, which cross the shadow boundary, or hand a whole theme to the desktop's .styles.
When a <wm-window> is the active member of a tab group with two or more members, its titlebar shows a tab strip in place of the title: [part=tabs][role=tablist] with a button[role=tab][data-window-id] per member. A click, Enter or Space activates a tab; ArrowLeft and ArrowRight (wrapping), Home and End move focus between tabs. messages.tabs names the tab list ('Tabs', 'Вкладки' in Russian). A group member that is not a <wm-window> still shows up as a tab, but it has no strip of its own to switch back from: offer the keyboard shortcut (Ctrl/⌘ + Alt + PageDown / PageUp) or a control of your own.
Events bubble and cross shadow roots. Each detail carries the window:
| Event | When |
|---|---|
wm-ready (on the desktop) | the manager exists; detail.manager |
wm-open, wm-close | the window opened or closed |
wm-beforeclose | a close was asked for; preventDefault() keeps the window open |
wm-move, wm-resize, wm-stage, wm-focus | geometry, stage or focus changed |
The entry also exports defineWmElements, the element classes WmDesktopElement and WmWindowElement, the label catalogues elementsMessages and elementsMessagesRu, and the types ElementsMessages, WmDesktopOptions, WmDesktopStyles and DefineOptions.
Choosing an entry point
| You want | Use |
|---|---|
| full control, no framework | attachDesktop directly |
| a framework lifecycle to own the wiring | the matching adapter |
| several desktops on one page | one manager and one binder per desktop |
| a desktop rendered by one framework, windows by another | createDesktopBinder shared between them |
| no DOM at all (tests, SSR, a native renderer) | createWindowManager alone |