Problem: the hexagonal refactor churns the backend tree for nine more phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper, compose/web image) must move to its own repo first so doc/layout rewrites land once on a backend-only tree. Change: - New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/ (updateURL repointed to oikos-web releases), compose/, own CI (web + desktop jobs), own deploy script (CI-green gate, TOCTOU guard, version-tagged images, prune-to-3), own webhook receiver on :9798 + launchd unit, own compose project publishing the same 8091:80. - Cutover executed on mac-mini in order: oikos stack's web service stopped+removed, oikos-web project brought up on 8091; outer Caddy untouched (targets the published port) — serving + Authentik flow + /wails 404 quirk verified post-cutover. - Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install targets, the compose web service, oikos-web from deploy.sh's fallback prune list; wails + go-keyring dropped from go.mod, vendor synced. - README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now point at the new repo; mbse + mascot design docs carry a path note. Risk: production SPA serving depends on the new pipeline now; rollback is versioned-image re-up of the old web service from a pre-split checkout (port 8091). Desktop builds installed before the split still check dtoro/oikos releases — one manual reinstall, noted in the oikos-web release notes. Verification: go vet, make test (race), make generate-check, golangci (no new findings; baseline down 400→365); post-cutover curls — localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
140 lines
9.9 KiB
Markdown
140 lines
9.9 KiB
Markdown
# wmkit
|
||
|
||
**Headless оконный менеджер для веба.** Перетаскиваемые окна с ресайзом, снэпом, таскбаром, клавиатурной доступностью и персистом состояния — для vanilla JS и всех основных фреймворков.
|
||
|
||
[English version](./README.md) · [Живое демо](https://wmkit.vercel.app) · [Зеркало (Pages)](https://surdeddd.github.io/wmkit/) · [GitHub](https://github.com/Surdeddd/wmkit)
|
||
|
||
[](https://surdeddd.github.io/wmkit/)
|
||
|
||
<p align="center"><em>Все окна выше настоящие — <a href="https://surdeddd.github.io/wmkit/">откройте демо</a> и потаскайте.</em></p>
|
||
|
||
- 🪟 **Полный жизненный цикл окна** — открытие, закрытие, фокус, сворачивание, разворачивание, восстановление, drag, ресайз в 8 направлениях
|
||
- 🧠 **Headless-ядро** — сериализуемая стейт-машина плюс DOM-контроллер; своя разметка или готовая стеклянная тема
|
||
- ⚛️ **Родные адаптеры** — `@surdeddd/wmkit/react`, `/vue`, `/svelte`, `/solid`, `/angular`, тонкий сахар над одним ядром
|
||
- ⊞ **Snap-зоны** — половины, четверти и максимизация от верхнего края с живым превью
|
||
- 🧲 **Магнетизм** — края окна прилипают к соседям и вьюпорту при перетаскивании
|
||
- ↩️ **Undo/redo** — каждая мутация = один шаг; целый drag схлопывается в одну запись истории
|
||
- 🗂️ **Именованные layout'ы и arrange** — снапшоты рабочего стола, `cascade`/`tile` одним вызовом
|
||
- ⌨️ **Доступность** — move/resize с клавиатуры, цикл окон по F6, focus-trap в модалках, `aria-live`-анонсы
|
||
- ⚡ **Производительность** — позиционирование только через `transform`, rAF-батчинг ввода, structural sharing; 50 окон таскаются на 60fps
|
||
- 💾 **Персист** — один вызов сериализует рабочий стол, один — восстанавливает
|
||
- 🎨 **Три темы** — тёмное стекло, светлое стекло и Win98-ретро, либо полностью свой CSS
|
||
- 🖼️ **Popout** *(experimental)* — вынос окна в Document Picture-in-Picture
|
||
- 📦 **Ноль зависимостей**, строгий TypeScript, ESM + CJS, ~9.3 kB brotli
|
||
|
||
## Установка
|
||
|
||
```bash
|
||
npm install @surdeddd/wmkit
|
||
# или
|
||
pnpm add @surdeddd/wmkit
|
||
```
|
||
|
||
## Быстрый старт (vanilla)
|
||
|
||
```js
|
||
import { createWindowManager, attachDesktop } from '@surdeddd/wmkit'
|
||
import '@surdeddd/wmkit/themes/glass.css'
|
||
|
||
const wm = createWindowManager()
|
||
const desktop = attachDesktop(wm, document.querySelector('#desktop'))
|
||
|
||
const win = wm.open({ title: 'Привет', width: 420, height: 280 })
|
||
|
||
const el = document.createElement('section')
|
||
el.innerHTML = `
|
||
<header data-wm-drag>
|
||
<span data-wm-title>Привет</span>
|
||
<span data-wm-controls>
|
||
<button data-wm-minimize aria-label="Свернуть"></button>
|
||
<button data-wm-maximize aria-label="Развернуть"></button>
|
||
<button data-wm-close aria-label="Закрыть"></button>
|
||
</span>
|
||
</header>
|
||
<div data-wm-content>Что угодно.</div>
|
||
`
|
||
document.querySelector('#desktop').append(el)
|
||
desktop.attachWindow(win.id, el, { removeOnClose: true })
|
||
```
|
||
|
||
Элемент рабочего стола становится системой координат. Разметка остаётся вашей — wmkit вешает поведение на `data-wm-*` атрибуты:
|
||
|
||
| Атрибут | Смысл |
|
||
| --- | --- |
|
||
| `data-wm-drag` | ручка перетаскивания (обычно тайтлбар); двойной клик — toggle maximize |
|
||
| `data-wm-title` | узел заголовка, связывается через `aria-labelledby` |
|
||
| `data-wm-close` / `data-wm-minimize` / `data-wm-maximize` | кнопки управления, работают через делегирование |
|
||
| `data-wm-content` | скроллируемая область контента |
|
||
|
||
`removeOnClose` сам отвязывает контроллер и удаляет элемент при закрытии окна. На тач-устройствах хит-зоны ресайза и порог снэпа автоматически крупнее (`pointer: coarse`); настраиваются через `attachDesktop(wm, el, { hitAreas: { edge, corner } })`.
|
||
|
||
Контроллер добавляет ресайз-хендлы (`[data-wm-resize]`), превью снэпа (`[data-wm-snap-preview]`) и скрытый live-регион для скринридеров.
|
||
|
||
## Адаптеры
|
||
|
||
Примеры для React, Vue, Svelte, Solid и Angular — в [английском README](./README.md#react) и на [лендинге](https://surdeddd.github.io/wmkit/) (табы «Фреймворки»). Принцип один: контент окна живёт в дереве вашего фреймворка, никакого innerHTML.
|
||
|
||
## API ядра — кратко
|
||
|
||
```ts
|
||
const wm = createWindowManager({ keepInViewport: true, defaultSize: { width: 480, height: 320 } })
|
||
|
||
wm.open({ id: 'docs', title: 'Документы', layer: 'floating' })
|
||
wm.snap('docs', 'left') // 'right' | 'top-left' | 'bottom-right' | …
|
||
wm.minimize('docs') // restore вернёт предыдущий stage, включая maximized/snapped
|
||
wm.update('docs', { title: 'Новый заголовок', meta: { pinned: true } })
|
||
|
||
const json = wm.serialize() // JSON-безопасный снапшот
|
||
wm.hydrate(json)
|
||
|
||
wm.on('stage', ({ window, previous }) => console.log(previous, '→', window.stage))
|
||
wm.batch(() => { /* много операций — одно уведомление */ })
|
||
|
||
wm.undo(); wm.redo() // история изменений, drag = одна запись (historyLimit, default 50)
|
||
wm.saveLayout('работа'); wm.loadLayout('работа') // именованные снапшоты рабочего стола
|
||
wm.arrange('tile') // или 'cascade'; плюс minimizeAll() / restoreAll()
|
||
```
|
||
|
||
Слои: `normal` < `floating` (always-on-top) < `modal`. Модалка блокирует фокус нижних окон (попытка — событие `modalblocked` и flash-анимация), Tab заперт внутри.
|
||
|
||
Клавиатура по умолчанию: стрелки двигают сфокусированное окно (16 px), `Alt` — шаг 1 px, `Shift+стрелки` — ресайз, `F6`/`Shift+F6` — цикл по окнам, `Escape` отменяет активный drag/resize.
|
||
|
||
Магнетизм включён из коробки (порог 8 px, на тач-устройствах 12 px): `attachDesktop(wm, el, { magnetism: { threshold: 16 } })` или `magnetism: false`. Свой контекст-меню тайтлбара — через `onTitlebarContextMenu(win, event)` (правый клик и long-press на таче).
|
||
|
||
### Персист
|
||
|
||
```js
|
||
import { persist } from '@surdeddd/wmkit/persist'
|
||
persist(wm, { key: 'my-desktop' }) // авто-восстановление + debounce-сохранение
|
||
```
|
||
|
||
### Popout (experimental)
|
||
|
||
```js
|
||
import { popout, isPopoutSupported } from '@surdeddd/wmkit/popout'
|
||
if (isPopoutSupported()) await popout(wm, 'docs', contentElement)
|
||
```
|
||
|
||
Окно уезжает в настоящее always-on-top окно ОС (Document Picture-in-Picture) с тем же JS-контекстом и состоянием.
|
||
|
||
## Темизация
|
||
|
||
Подключите `@surdeddd/wmkit/themes/glass.css` и переопределяйте CSS-переменные (`--wm-radius`, `--wm-bg`, `--wm-accent`, …) — или не подключайте ничего и стилизуйте `data-wm-stage`, `data-wm-focused`, `data-wm-dragging`, `[hidden]` сами.
|
||
|
||
Ещё две готовые темы: `themes/light.css` (светлое стекло) и `themes/retro.css` (Win98-ностальгия). Все три стилизуют одни и те же `data-wm-*` атрибуты — переключение = замена одного импорта.
|
||
|
||
## SSR
|
||
|
||
Ядро не трогает `window`/`document`: менеджер можно создавать и гидрейтить на сервере, `attachDesktop` вызывается после маунта. `persist` тихо выключается без доступного storage.
|
||
|
||
## Качество
|
||
|
||
- 172 юнит-теста, **100%** покрытие стейт-машины и persist по строкам/веткам/функциям
|
||
- 178+ Playwright-сценариев на Chromium, WebKit и мобильной эмуляции: drag, ресайз во все стороны, снэп, магнетизм, undo после drag, клавиатура, touch, персист через перезагрузку, стресс на 50 окон, модальные ловушки, axe-аудиты доступности, визуальная регрессия по скриншотам
|
||
- перф-бенчмарки в CI на каждый push (`vitest bench`): 1 000 окон открываются за ~150 мс, move среди 50 окон ~1.2 мкс, полный undo/redo-проход на 100 шагов ~52 мкс
|
||
- `publint` + `@arethetypeswrong/cli` проверяют валидность пакета, `size-limit` следит за бюджетами
|
||
|
||
## Лицензия
|
||
|
||
[MIT](./LICENSE) © Максим Кравцов
|