@surdeddd/wmkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/README.ru.md +120 -0
  4. package/dist/chunk-7HHDQEBI.js +1334 -0
  5. package/dist/chunk-7HHDQEBI.js.map +1 -0
  6. package/dist/chunk-KWLI7JIY.cjs +1349 -0
  7. package/dist/chunk-KWLI7JIY.cjs.map +1 -0
  8. package/dist/controller-B0bogK9N.d.cts +65 -0
  9. package/dist/controller-D5zIriLg.d.ts +65 -0
  10. package/dist/index.cjs +64 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +22 -0
  13. package/dist/index.d.ts +22 -0
  14. package/dist/index.js +3 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/persist.cjs +74 -0
  17. package/dist/persist.cjs.map +1 -0
  18. package/dist/persist.d.cts +22 -0
  19. package/dist/persist.d.ts +22 -0
  20. package/dist/persist.js +72 -0
  21. package/dist/persist.js.map +1 -0
  22. package/dist/popout.cjs +66 -0
  23. package/dist/popout.cjs.map +1 -0
  24. package/dist/popout.d.cts +28 -0
  25. package/dist/popout.d.ts +28 -0
  26. package/dist/popout.js +63 -0
  27. package/dist/popout.js.map +1 -0
  28. package/dist/react.cjs +57 -0
  29. package/dist/react.cjs.map +1 -0
  30. package/dist/react.d.cts +16 -0
  31. package/dist/react.d.ts +16 -0
  32. package/dist/react.js +51 -0
  33. package/dist/react.js.map +1 -0
  34. package/dist/solid.cjs +45 -0
  35. package/dist/solid.cjs.map +1 -0
  36. package/dist/solid.d.cts +15 -0
  37. package/dist/solid.d.ts +15 -0
  38. package/dist/solid.js +40 -0
  39. package/dist/solid.js.map +1 -0
  40. package/dist/svelte.cjs +60 -0
  41. package/dist/svelte.cjs.map +1 -0
  42. package/dist/svelte.d.cts +24 -0
  43. package/dist/svelte.d.ts +24 -0
  44. package/dist/svelte.js +55 -0
  45. package/dist/svelte.js.map +1 -0
  46. package/dist/themes/glass.css +203 -0
  47. package/dist/types-CtFzL_oA.d.cts +151 -0
  48. package/dist/types-CtFzL_oA.d.ts +151 -0
  49. package/dist/vue.cjs +54 -0
  50. package/dist/vue.cjs.map +1 -0
  51. package/dist/vue.d.cts +11 -0
  52. package/dist/vue.d.ts +11 -0
  53. package/dist/vue.js +48 -0
  54. package/dist/vue.js.map +1 -0
  55. package/package.json +257 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maksim Kravcov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # wmkit
2
+
3
+ **Headless window manager for the web.** Draggable, resizable, snappable windows with a taskbar model, keyboard accessibility and state persistence — for vanilla JS and every major framework.
4
+
5
+ [Русская версия](./README.ru.md) · [Live demo](https://surdeddd.github.io/wmkit/) · [GitHub](https://github.com/Surdeddd/wmkit)
6
+
7
+ [![CI](https://github.com/Surdeddd/wmkit/actions/workflows/ci.yml/badge.svg)](https://github.com/Surdeddd/wmkit/actions/workflows/ci.yml)
8
+ [![npm](https://img.shields.io/npm/v/@surdeddd/wmkit)](https://www.npmjs.com/package/@surdeddd/wmkit)
9
+ [![license](https://img.shields.io/badge/license-MIT-2dd4a8)](./LICENSE)
10
+
11
+ - 🪟 **Full window lifecycle** — open, close, focus, minimize, maximize, restore, drag, 8-direction resize
12
+ - 🧠 **Headless core** — a serializable state machine plus a DOM controller; bring your own markup or use the glass theme
13
+ - ⚛️ **Official adapters** — `@surdeddd/wmkit/react`, `@surdeddd/wmkit/vue`, `@surdeddd/wmkit/svelte`, `@surdeddd/wmkit/solid`, all thin sugar over one core
14
+ - ⊞ **Snap zones** — halves, quarters and drag-to-top maximize with a live preview
15
+ - ⌨️ **Accessible** — keyboard move/resize, F6 window cycling, focus-trapped modals, `aria-live` announcements
16
+ - ⚡ **Fast** — `transform`-only positioning, rAF-batched pointer input, structural sharing; 50 windows drag at 60fps
17
+ - 💾 **Persistence** — one call to serialize the desktop, one call to restore it
18
+ - 🖼️ **Popout** *(experimental)* — send a window into Document Picture-in-Picture
19
+ - 📦 **Zero dependencies**, strict TypeScript, ESM + CJS, ~9 kB gzip core
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm install @surdeddd/wmkit
25
+ # or
26
+ pnpm add @surdeddd/wmkit
27
+ ```
28
+
29
+ ## Quick start (vanilla)
30
+
31
+ ```js
32
+ import { createWindowManager, attachDesktop } from '@surdeddd/wmkit'
33
+ import '@surdeddd/wmkit/themes/glass.css'
34
+
35
+ const wm = createWindowManager()
36
+ const desktop = attachDesktop(wm, document.querySelector('#desktop'))
37
+
38
+ const win = wm.open({ title: 'Hello', width: 420, height: 280 })
39
+
40
+ const el = document.createElement('section')
41
+ el.innerHTML = `
42
+ <header data-wm-drag>
43
+ <span data-wm-title>Hello</span>
44
+ <span data-wm-controls>
45
+ <button data-wm-minimize aria-label="Minimize"></button>
46
+ <button data-wm-maximize aria-label="Maximize"></button>
47
+ <button data-wm-close aria-label="Close"></button>
48
+ </span>
49
+ </header>
50
+ <div data-wm-content>Anything you want.</div>
51
+ `
52
+ document.querySelector('#desktop').append(el)
53
+ desktop.attachWindow(win.id, el)
54
+ ```
55
+
56
+ The desktop element becomes the coordinate space. Your markup stays yours — wmkit wires behavior onto `data-wm-*` attributes:
57
+
58
+ | Attribute | Meaning |
59
+ | --- | --- |
60
+ | `data-wm-drag` | drag handle (usually the titlebar); double-click toggles maximize |
61
+ | `data-wm-title` | window title node, linked via `aria-labelledby` |
62
+ | `data-wm-close` / `data-wm-minimize` / `data-wm-maximize` | control buttons, wired by delegation |
63
+ | `data-wm-content` | scrollable content area (styled by themes) |
64
+
65
+ The controller adds resize handles (`[data-wm-resize]`), a snap preview (`[data-wm-snap-preview]`) and a visually hidden live region for screen readers.
66
+
67
+ ## React
68
+
69
+ ```tsx
70
+ import { useWindowManager, useDesktop, useWmState, useWmWindowRef } from '@surdeddd/wmkit/react'
71
+ import '@surdeddd/wmkit/themes/glass.css'
72
+
73
+ function Desktop() {
74
+ const wm = useWindowManager()
75
+ const { ref, binder } = useDesktop(wm)
76
+ const state = useWmState(wm)
77
+
78
+ return (
79
+ <div ref={ref} style={{ position: 'relative', height: '100vh' }}>
80
+ <button onClick={() => wm.open({ title: 'New window' })}>open</button>
81
+ {state.order.map((id) => {
82
+ const win = state.windows[id]
83
+ return win ? <Win key={id} binder={binder} win={win} /> : null
84
+ })}
85
+ </div>
86
+ )
87
+ }
88
+
89
+ function Win({ binder, win }) {
90
+ const ref = useWmWindowRef(binder, win.id)
91
+ return (
92
+ <section ref={ref}>
93
+ <header data-wm-drag>
94
+ <span data-wm-title>{win.title}</span>
95
+ <span data-wm-controls>
96
+ <button data-wm-minimize aria-label="Minimize" />
97
+ <button data-wm-maximize aria-label="Maximize" />
98
+ <button data-wm-close aria-label="Close" />
99
+ </span>
100
+ </header>
101
+ <div data-wm-content>Your React tree lives here — no portals, no innerHTML.</div>
102
+ </section>
103
+ )
104
+ }
105
+ ```
106
+
107
+ `useWmState` subscribes through `useSyncExternalStore`; unchanged windows keep referential identity, so memoized children skip re-renders.
108
+
109
+ ## Vue
110
+
111
+ ```vue
112
+ <script setup>
113
+ import { ref } from 'vue'
114
+ import { useWindowManager, useDesktop, useWmWindowEl, useWmState } from '@surdeddd/wmkit/vue'
115
+ import '@surdeddd/wmkit/themes/glass.css'
116
+
117
+ const wm = useWindowManager()
118
+ const desktopEl = ref(null)
119
+ const binder = useDesktop(wm, desktopEl)
120
+ const state = useWmState(wm)
121
+
122
+ const noteEl = ref(null)
123
+ useWmWindowEl(binder, 'note', noteEl)
124
+ wm.open({ id: 'note', title: 'Note' })
125
+ </script>
126
+
127
+ <template>
128
+ <div ref="desktopEl" style="position: relative; height: 100vh">
129
+ <section ref="noteEl">
130
+ <header data-wm-drag><span data-wm-title>{{ state.windows.note?.title }}</span></header>
131
+ <div data-wm-content>composables all the way down</div>
132
+ </section>
133
+ </div>
134
+ </template>
135
+ ```
136
+
137
+ ## Svelte
138
+
139
+ ```svelte
140
+ <script>
141
+ import { createManager, createDesktop, wmWindowStore } from '@surdeddd/wmkit/svelte'
142
+ import '@surdeddd/wmkit/themes/glass.css'
143
+
144
+ const wm = createManager()
145
+ const dk = createDesktop(wm)
146
+ wm.open({ id: 'main', title: 'Hello' })
147
+ const main = wmWindowStore(wm, 'main')
148
+ </script>
149
+
150
+ <div use:dk.desktop style="position: relative; height: 100vh">
151
+ <section use:dk.window={{ id: 'main' }}>
152
+ <header data-wm-drag><span data-wm-title>{$main?.title}</span></header>
153
+ <div data-wm-content>stores and actions, no wrapper components</div>
154
+ </section>
155
+ </div>
156
+ ```
157
+
158
+ ## Solid
159
+
160
+ ```tsx
161
+ import { For } from 'solid-js'
162
+ import { useWindowManager, createDesktop, useWmState } from '@surdeddd/wmkit/solid'
163
+
164
+ function Desktop() {
165
+ const wm = useWindowManager()
166
+ const dk = createDesktop(wm)
167
+ const state = useWmState(wm)
168
+ wm.open({ title: 'Hello' })
169
+
170
+ return (
171
+ <div ref={dk.desktop} style={{ position: 'relative', height: '100vh' }}>
172
+ <For each={state().order}>
173
+ {(id) => (
174
+ <section ref={dk.window(id)}>
175
+ <header data-wm-drag>
176
+ <span data-wm-title>{state().windows[id]?.title}</span>
177
+ </header>
178
+ <div data-wm-content>fine-grained, obviously</div>
179
+ </section>
180
+ )}
181
+ </For>
182
+ </div>
183
+ )
184
+ }
185
+ ```
186
+
187
+ ## Core API
188
+
189
+ ### `createWindowManager(options?)`
190
+
191
+ Pure state machine — no DOM access, safe to create during SSR.
192
+
193
+ ```ts
194
+ interface ManagerOptions {
195
+ viewport?: { width: number; height: number }
196
+ keepInViewport?: boolean // clamp windows so the titlebar stays reachable (default true)
197
+ minVisible?: number // minimum visible strip in px (default 48)
198
+ defaultSize?: { width: number; height: number }
199
+ cascadeOffset?: number // auto-position step for new windows (default 32)
200
+ cascadeOrigin?: { x: number; y: number }
201
+ idPrefix?: string
202
+ }
203
+ ```
204
+
205
+ Manager methods:
206
+
207
+ | Method | Notes |
208
+ | --- | --- |
209
+ | `open(init?)` → `WindowState` | throws on duplicate `id`; cascades position when `x`/`y` omitted |
210
+ | `close(id)` / `closeAll()` | focus moves to the next eligible window |
211
+ | `focus(id)` / `blur()` / `cycleFocus(dir?)` | focusing a minimized window restores it; modals block focus below them |
212
+ | `minimize(id)` / `maximize(id)` / `restore(id)` / `toggleMaximize(id)` | restore returns to the pre-minimize stage, including maximized/snapped |
213
+ | `snap(id, zone)` | `'left' \| 'right' \| 'top' \| 'bottom' \| 'top-left' \| …` |
214
+ | `move(id, x, y)` / `moveBy(id, dx, dy)` / `resize(id, patch)` | resizing a snapped window unsnaps it |
215
+ | `restoreTo(id, bounds)` | used for drag-off-snap; stage → `normal` at explicit bounds |
216
+ | `update(id, patch)` | title, layer, min/max size, per-window flags, `meta` |
217
+ | `setViewport(size)` | re-derives maximized/snapped bounds, clamps the rest |
218
+ | `serialize()` / `hydrate(data)` | JSON-safe snapshot of the whole desktop |
219
+ | `subscribe(fn)` / `on(event, fn)` | granular events: `open, close, focus, move, resize, stage, update, order, modalblocked` |
220
+ | `batch(fn)` | coalesce many operations into one `change` notification |
221
+
222
+ Windows carry `layer: 'normal' | 'floating' | 'modal'` — floating stays on top, modals trap focus and block interaction below (blocked attempts emit `modalblocked` and flash the modal).
223
+
224
+ ### `attachDesktop(wm, element, options?)`
225
+
226
+ DOM controller: pointer drag with capture (touch/pen included), 8-direction resize, snap detection with preview, keyboard handling, ARIA wiring, FLIP-to-taskbar animation.
227
+
228
+ ```ts
229
+ interface DesktopOptions {
230
+ snap?: boolean | { threshold?: number; cornerSize?: number; preview?: boolean; topEdge?: 'maximize' | 'top' | 'none' }
231
+ keyboard?: boolean | { moveStep?: number; cycle?: boolean }
232
+ announce?: boolean | Partial<AnnouncerMessages> // localize screen-reader strings here
233
+ autoViewport?: boolean // ResizeObserver → wm.setViewport (default true)
234
+ minimizeTarget?: (win: WindowState) => Element | null // FLIP ghost target on minimize
235
+ }
236
+ ```
237
+
238
+ Keyboard defaults: arrows move the focused window (16 px), `Alt` for 1 px steps, `Shift+arrows` resize, `F6` / `Shift+F6` cycle windows, `Escape` cancels an in-flight drag or resize.
239
+
240
+ ### `persist(wm, options?)` — `@surdeddd/wmkit/persist`
241
+
242
+ ```js
243
+ import { persist } from '@surdeddd/wmkit/persist'
244
+
245
+ const store = persist(wm, { key: 'my-desktop' }) // auto-restores, then debounce-saves on change
246
+ store.clear()
247
+ ```
248
+
249
+ Storage defaults to `localStorage` (probed safely — SSR and private-mode friendly) and accepts any `getItem/setItem/removeItem` implementation.
250
+
251
+ ### `popout(wm, id, contentEl, options?)` — `@surdeddd/wmkit/popout` *(experimental)*
252
+
253
+ Moves a window's content into a [Document Picture-in-Picture](https://developer.mozilla.org/docs/Web/API/Document_Picture-in-Picture_API) always-on-top OS window, keeping the same JS context and state. Feature-detect with `isPopoutSupported()`.
254
+
255
+ ## Theming
256
+
257
+ `@surdeddd/wmkit/themes/glass.css` styles the `data-wm-*` attributes and exposes CSS variables:
258
+
259
+ ```css
260
+ [data-wm-desktop] {
261
+ --wm-radius: 14px;
262
+ --wm-bg: rgba(22, 24, 34, 0.55);
263
+ --wm-accent: #7c6cff;
264
+ /* --wm-border, --wm-shadow, --wm-titlebar-bg, --wm-text, --wm-blur, --wm-transition … */
265
+ }
266
+ ```
267
+
268
+ Skip the import entirely and the library stays headless: state attributes (`data-wm-stage`, `data-wm-focused`, `data-wm-dragging`, `data-wm-flash`, `[hidden]`) are yours to style.
269
+
270
+ ## SSR
271
+
272
+ The core never touches `window`/`document` — create managers and even `hydrate()` state on the server, then call `attachDesktop` after mount. `persist` no-ops without usable storage.
273
+
274
+ ## Comparison
275
+
276
+ | | wmkit | WinBox | jsPanel4 | Dockview | Zag floating-panel |
277
+ | --- | --- | --- | --- | --- | --- |
278
+ | Maintained | ✓ 2026 | ✗ since 2023 | ✗ since 2022 | ✓ | ✓ |
279
+ | Headless core | ✓ | ✗ | ✗ | ~ own UI | ✓ |
280
+ | Official adapters | React·Vue·Svelte·Solid | community | ✗ | React·Vue·Angular | via Ark UI |
281
+ | Multi-window (z-order, taskbar, modals) | ✓ | partial | partial | dock groups | ✗ single panel |
282
+ | Snap zones + preview | ✓ | ✗ | ✗ | — | ✗ |
283
+ | Keyboard + screen reader | ✓ | ✗ | ✗ | partial | partial |
284
+ | Persistence built in | ✓ | ✗ | ✗ | ✓ | ✗ |
285
+ | Document PiP popout | ✓ | ✗ | ✗ | window.open | ✗ |
286
+ | TypeScript | strict | @types | ✗ | ✓ | ✓ |
287
+
288
+ *(checked July 2026: commit history, npm downloads, open feature requests)*
289
+
290
+ ## Quality
291
+
292
+ - 121 unit tests, **100%** line/branch/function/statement coverage on the core state machine and persistence
293
+ - 160+ Playwright scenarios on Chromium, WebKit and mobile emulation: drag, 8-way resize, snap, keyboard, touch, persistence across reloads, 50-window stress, modal traps, axe accessibility scans
294
+ - `publint` + `@arethetypeswrong/cli` validate the published package, `size-limit` guards bundle budgets
295
+
296
+ ## Development
297
+
298
+ ```bash
299
+ pnpm install
300
+ pnpm dev # landing + playground on Vite
301
+ pnpm test # unit tests
302
+ pnpm test:e2e # Playwright matrix
303
+ pnpm verify # the full gate: lint, types, coverage, build, size, publint, e2e
304
+ ```
305
+
306
+ ## License
307
+
308
+ [MIT](./LICENSE) © Maksim Kravcov
package/README.ru.md ADDED
@@ -0,0 +1,120 @@
1
+ # wmkit
2
+
3
+ **Headless оконный менеджер для веба.** Перетаскиваемые окна с ресайзом, снэпом, таскбаром, клавиатурной доступностью и персистом состояния — для vanilla JS и всех основных фреймворков.
4
+
5
+ [English version](./README.md) · [Живое демо](https://surdeddd.github.io/wmkit/) · [GitHub](https://github.com/Surdeddd/wmkit)
6
+
7
+ - 🪟 **Полный жизненный цикл окна** — открытие, закрытие, фокус, сворачивание, разворачивание, восстановление, drag, ресайз в 8 направлениях
8
+ - 🧠 **Headless-ядро** — сериализуемая стейт-машина плюс DOM-контроллер; своя разметка или готовая стеклянная тема
9
+ - ⚛️ **Родные адаптеры** — `@surdeddd/wmkit/react`, `@surdeddd/wmkit/vue`, `@surdeddd/wmkit/svelte`, `@surdeddd/wmkit/solid`, тонкий сахар над одним ядром
10
+ - ⊞ **Snap-зоны** — половины, четверти и максимизация от верхнего края с живым превью
11
+ - ⌨️ **Доступность** — move/resize с клавиатуры, цикл окон по F6, focus-trap в модалках, `aria-live`-анонсы
12
+ - ⚡ **Производительность** — позиционирование только через `transform`, rAF-батчинг ввода, structural sharing; 50 окон таскаются на 60fps
13
+ - 💾 **Персист** — один вызов сериализует рабочий стол, один — восстанавливает
14
+ - 🖼️ **Popout** *(experimental)* — вынос окна в Document Picture-in-Picture
15
+ - 📦 **Ноль зависимостей**, строгий TypeScript, ESM + CJS, ~9 kB gzip
16
+
17
+ ## Установка
18
+
19
+ ```bash
20
+ npm install @surdeddd/wmkit
21
+ # или
22
+ pnpm add @surdeddd/wmkit
23
+ ```
24
+
25
+ ## Быстрый старт (vanilla)
26
+
27
+ ```js
28
+ import { createWindowManager, attachDesktop } from '@surdeddd/wmkit'
29
+ import '@surdeddd/wmkit/themes/glass.css'
30
+
31
+ const wm = createWindowManager()
32
+ const desktop = attachDesktop(wm, document.querySelector('#desktop'))
33
+
34
+ const win = wm.open({ title: 'Привет', width: 420, height: 280 })
35
+
36
+ const el = document.createElement('section')
37
+ el.innerHTML = `
38
+ <header data-wm-drag>
39
+ <span data-wm-title>Привет</span>
40
+ <span data-wm-controls>
41
+ <button data-wm-minimize aria-label="Свернуть"></button>
42
+ <button data-wm-maximize aria-label="Развернуть"></button>
43
+ <button data-wm-close aria-label="Закрыть"></button>
44
+ </span>
45
+ </header>
46
+ <div data-wm-content>Что угодно.</div>
47
+ `
48
+ document.querySelector('#desktop').append(el)
49
+ desktop.attachWindow(win.id, el)
50
+ ```
51
+
52
+ Элемент рабочего стола становится системой координат. Разметка остаётся вашей — wmkit вешает поведение на `data-wm-*` атрибуты:
53
+
54
+ | Атрибут | Смысл |
55
+ | --- | --- |
56
+ | `data-wm-drag` | ручка перетаскивания (обычно тайтлбар); двойной клик — toggle maximize |
57
+ | `data-wm-title` | узел заголовка, связывается через `aria-labelledby` |
58
+ | `data-wm-close` / `data-wm-minimize` / `data-wm-maximize` | кнопки управления, работают через делегирование |
59
+ | `data-wm-content` | скроллируемая область контента |
60
+
61
+ Контроллер добавляет ресайз-хендлы (`[data-wm-resize]`), превью снэпа (`[data-wm-snap-preview]`) и скрытый live-регион для скринридеров.
62
+
63
+ ## Адаптеры
64
+
65
+ Примеры для React, Vue, Svelte и Solid — в [английском README](./README.md#react) и на [лендинге](https://surdeddd.github.io/wmkit/) (табы «Фреймворки»). Принцип один: контент окна живёт в дереве вашего фреймворка, никакого innerHTML.
66
+
67
+ ## API ядра — кратко
68
+
69
+ ```ts
70
+ const wm = createWindowManager({ keepInViewport: true, defaultSize: { width: 480, height: 320 } })
71
+
72
+ wm.open({ id: 'docs', title: 'Документы', layer: 'floating' })
73
+ wm.snap('docs', 'left') // 'right' | 'top-left' | 'bottom-right' | …
74
+ wm.minimize('docs') // restore вернёт предыдущий stage, включая maximized/snapped
75
+ wm.update('docs', { title: 'Новый заголовок', meta: { pinned: true } })
76
+
77
+ const json = wm.serialize() // JSON-безопасный снапшот
78
+ wm.hydrate(json)
79
+
80
+ wm.on('stage', ({ window, previous }) => console.log(previous, '→', window.stage))
81
+ wm.batch(() => { /* много операций — одно уведомление */ })
82
+ ```
83
+
84
+ Слои: `normal` < `floating` (always-on-top) < `modal`. Модалка блокирует фокус нижних окон (попытка — событие `modalblocked` и flash-анимация), Tab заперт внутри.
85
+
86
+ Клавиатура по умолчанию: стрелки двигают сфокусированное окно (16 px), `Alt` — шаг 1 px, `Shift+стрелки` — ресайз, `F6`/`Shift+F6` — цикл по окнам, `Escape` отменяет активный drag/resize.
87
+
88
+ ### Персист
89
+
90
+ ```js
91
+ import { persist } from '@surdeddd/wmkit/persist'
92
+ persist(wm, { key: 'my-desktop' }) // авто-восстановление + debounce-сохранение
93
+ ```
94
+
95
+ ### Popout (experimental)
96
+
97
+ ```js
98
+ import { popout, isPopoutSupported } from '@surdeddd/wmkit/popout'
99
+ if (isPopoutSupported()) await popout(wm, 'docs', contentElement)
100
+ ```
101
+
102
+ Окно уезжает в настоящее always-on-top окно ОС (Document Picture-in-Picture) с тем же JS-контекстом и состоянием.
103
+
104
+ ## Темизация
105
+
106
+ Подключите `@surdeddd/wmkit/themes/glass.css` и переопределяйте CSS-переменные (`--wm-radius`, `--wm-bg`, `--wm-accent`, …) — или не подключайте ничего и стилизуйте `data-wm-stage`, `data-wm-focused`, `data-wm-dragging`, `[hidden]` сами.
107
+
108
+ ## SSR
109
+
110
+ Ядро не трогает `window`/`document`: менеджер можно создавать и гидрейтить на сервере, `attachDesktop` вызывается после маунта. `persist` тихо выключается без доступного storage.
111
+
112
+ ## Качество
113
+
114
+ - 121 юнит-тест, **100%** покрытие стейт-машины и persist по строкам/веткам/функциям
115
+ - 160+ Playwright-сценариев на Chromium, WebKit и мобильной эмуляции: drag, ресайз во все стороны, снэп, клавиатура, touch, персист через перезагрузку, стресс на 50 окон, модальные ловушки, axe-аудиты доступности
116
+ - `publint` + `@arethetypeswrong/cli` проверяют валидность пакета, `size-limit` следит за бюджетами
117
+
118
+ ## Лицензия
119
+
120
+ [MIT](./LICENSE) © Максим Кравцов