Files
oikos/web/node_modules/@surdeddd/wmkit/README.ru.md
dtoro d4d99a7473
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
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.
2026-08-15 22:27:52 +02:00

9.9 KiB
Raw Blame History

wmkit

Headless оконный менеджер для веба. Перетаскиваемые окна с ресайзом, снэпом, таскбаром, клавиатурной доступностью и персистом состояния — для vanilla JS и всех основных фреймворков.

English version · Живое демо · Зеркало (Pages) · GitHub

wmkit — живой демо-десктоп

Все окна выше настоящие — откройте демо и потаскайте.

  • 🪟 Полный жизненный цикл окна — открытие, закрытие, фокус, сворачивание, разворачивание, восстановление, 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

Установка

npm install @surdeddd/wmkit
# или
pnpm add @surdeddd/wmkit

Быстрый старт (vanilla)

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 и на лендинге (табы «Фреймворки»). Принцип один: контент окна живёт в дереве вашего фреймворка, никакого innerHTML.

API ядра — кратко

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 на таче).

Персист

import { persist } from '@surdeddd/wmkit/persist'
persist(wm, { key: 'my-desktop' })  // авто-восстановление + debounce-сохранение

Popout (experimental)

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 © Максим Кравцов