diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed9c2cc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+node_modules/
+dist/
+*.tsbuildinfo
+
+# секреты и локальные настройки
+.env
+.env.local
+.env.*.local
+
+# локальные данные (sqlite и пр.)
+data/
+
+.DS_Store
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b752d93
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+# Idle-XBOCT
+
+Idle-игра для Twitch-чата: аватары зрителей живут на экране стрима, команды чата
+управляют ими. План и все решения — в [docs/PLAN.md](docs/PLAN.md).
+
+## Запуск (M0 — каркас)
+
+```bash
+pnpm install
+pnpm dev
+```
+
+- сервер: http://localhost:3000 — WS на `/ws`, здоровье на `/healthz`
+- overlay (browser source для OBS): http://localhost:5173
+- web-мир: http://localhost:5174
+
+Сборка клиентов (`pnpm build`) — сервер начнёт раздавать их сам: `/overlay/` и `/web/`
+на порту 3000 (это режим для OBS, без vite dev-сервера).
+
+Twitch-подключение — с M1, чек-лист получения токенов: [docs/twitch-setup.md](docs/twitch-setup.md).
diff --git a/docs/PLAN.md b/docs/PLAN.md
index d378c92..4aa87ba 100644
--- a/docs/PLAN.md
+++ b/docs/PLAN.md
@@ -132,9 +132,10 @@ XP-кривая Melvor-подобная, уровни открывают нод
- **M0 — каркас.** Монорепа, overlay+web скелеты, WS-эхо.
✅ аватар-заглушка виден в OBS
-- **M1 — вертикальный срез.** `!рубить`/`!стоп`, спавн аватара (аватарка из тегов чата),
- ходьба к дереву, брёвна + XP + уровень, автосейв, WS-снапшот+дельты,
- камера-окно + простейший автотур.
+- **M1 — вертикальный срез.** `!рубить`/`!стоп`, спавн аватара (спрайт-человечек, ник
+ над головой в цвете чата), ходьба к дереву, брёвна + XP + уровень, автосейв,
+ WS-снапшот+дельты, камера-окно + простейший автотур, FakeTwitchSource — отладка
+ команд без живого чата.
✅ зритель пишет `!рубить` и видит себя рубящим в лесу
- **M2 — руда и кузница.** `!копать` + `!ковать` + тиры топоров/кирок.
✅ скрафтил железный топор → рубишь заметно быстрее
@@ -152,29 +153,36 @@ XP-кривая Melvor-подобная, уровни открывают нод
системы оборачиваем тонким интерфейсом для дешёвого fallback
- **OBS browser source кэширует**: dev через отдельный порт, версионирование протокола
в рукопожатии клиента/сервера
-- **Аватарки Twitch CDN**: ленивая загрузка + фолбэк-спрайт
+- **Free-версии паков (Sprout Lands, Mystic Woods) — только некоммерческое**:
+ для монетизированного канала — CC0 или платная лицензия
- **Rate limits чата**: бот отвечает дозированно
- **EventSub**: нужен Twitch-апп (client id/secret) у стримера; EventSub WebSocket
работает локально без публичного webhook-URL — ок для локального MVP
## Ассеты
-Аватар зрителя = фото профиля Twitch в кольце; спрайтовые человечки — «тела» под ними
-и фолбэк. Поэтому берём паки, где тело и голова разделены.
+Аватар зрителя = обычный спрайт-человечек целиком — лицо и тело из пака, фото профиля
+Twitch не используем. На старте все зрители с одним и тем же спрайтом; личность читается
+по нику над головой в цвете ника чата. Разные скины — на будущее (донатеры/сабы), поэтому
+пак желателен с запасом: генератор персонажей или палитрные свопы.
### Человечки + скины
-- **LimeZu — Modern Interiors / Modern Exteriors** (itch.io, ~$10–15) — основной кандидат:
- генератор персонажей с сотнями скинов/одежды, домики, мебель, здания уровня «ратуша»,
- всё в одном стиле
+- **LimeZu — Modern Interiors / Modern Exteriors** (itch.io, name-your-own-price ≈ $5,
+ лицензия CC-BY: коммерция ок с указанием авторства) — основной кандидат: домики, мебель,
+ здания уровня «ратуша», предметы — всё в одном стиле; генератор персонажей в платной
+ версии пригодится, когда дойдёт до скинов донатеров
- **Universal LPC Spritesheet Generator** (бесплатно, онлайн) — сборка спрайт-листов
с ходьбой/анимациями из тысяч деталей. Лицензия GPL/CC-BY-SA — проверить перед коммерциализацией
-- **Kenney.nl** — CC0, человечки простые геометрические — для прототипа M0–M1
+- **Kenney.nl** — CC0 (тайлы, предметы, простые человечки) — **текущая база проекта**;
+ конкретные паки подбираем при M1 (нужны: персонаж с ходьбой, деревья/камни, вода)
### Домики, костёр, ратуша
-- **Sprout Lands** (Cup Nooble, itch.io) — милый фермерский стиль
-- **Mystic Woods** (Game Endeavor, itch.io) — есть бесплатная lite-версия: домики, костёр, природа
+- **Sprout Lands** (Cup Nooble, itch.io) — милый фермерский стиль; free-версия только
+ для некоммерческого, premium от ~$4 снимает ограничение
+- **Mystic Woods** (Game Endeavor, itch.io) — есть бесплатная lite-версия: домики, костёр,
+ природа; free-версия тоже только для некоммерческого
- **OpenGameArt.org** — много RPG-паков зданий, лицензии смешанные — читать каждую
- **CraftPix freebies** — бесплатные наборы зданий/персонажей
@@ -184,11 +192,14 @@ XP-кривая Melvor-подобная, уровни открывают нод
CC-BY только с готовностью указать авторство (можно в дашборде)
2. Один стиль важнее качества каждого пака — брать линейку у одного автора
(LimeZu закрывает людей, дома, ратушу и предметы)
-3. Прототип M0–M1 — на CC0/Kenney, красивые паки подключаем с M2+
+3. **Решено: идём полностью на бесплатном.** База — Kenney (CC0); OpenGameArt — с проверкой
+ лицензии каждого ассета. Платные паки (LimeZu ≈ $5) — опция на потом, если захочется
+ единого стиля
## Идеи на потом
-- FakeTwitchSource: генератор фейковых зрителей/команд для разработки без живого стрима
+- Скины для донатеров/сабов: разные внешности аватаров (генератор персонажей LimeZu
+ или палитрные свопы)
- Событие усталости: аватар временно перестаёт работать, зритель реагирует
- Совместная работа: несколько аватаров у одной ноды дают небольшой бонус (социальная связка)
- Рандомные события для удержания: золотая жила, дух леса и т.п.
diff --git a/docs/twitch-setup.md b/docs/twitch-setup.md
new file mode 100644
index 0000000..14e2bb9
--- /dev/null
+++ b/docs/twitch-setup.md
@@ -0,0 +1,49 @@
+# Twitch: чек-лист подключения (нужен к M1)
+
+Бот-аккаунт уже есть. Осталось зарегистрировать приложение и получить токены —
+примерно 10 минут действий, делается один раз.
+
+## 1. Приложение (один раз)
+
+1. На аккаунте, который регистрирует приложение (можно сам бот-аккаунт), включите
+ 2FA: twitch.tv → Настройки → Безопасность и конфиденциальность. Без 2FA
+ Twitch не даёт создавать приложения.
+2. Зайти на https://dev.twitch.tv/console под этим аккаунтом → **Register Your Application**:
+ - Name: любое уникальное, например `idle-xboct-bot`
+ - OAuth Redirect URLs: `http://localhost:3000/oauth/callback`
+ - Category: **Chat Bot**
+3. Скопировать **Client ID**, затем Manage → **New Secret** → **Client Secret**
+ (показывается один раз).
+
+## 2. Токен бота — чтение и ответы в чате (M1)
+
+Scope: `chat:read chat:edit`.
+
+- Быстрый способ: https://twitchtokengenerator.com → «Chat Bot / Quick Token» →
+ залогиниться **именно бот-аккаунтом** → получить access token и refresh token.
+- Надёжный способ (когда дойдём до M1): мини-OAuth-скрипт на redirect из п.1.
+
+## 3. Токен стримера — Channel Points (нужен только к M5, спотлайт)
+
+Scope: `channel:read:redemptions`. Получается так же (twitchtokengenerator или свой
+OAuth), но залогиниться нужно **аккаунтом стримера**. EventSub-события выкупа наград
+принимают только пользовательский токен владельца канала — токена бота тут недостаточно.
+
+## 4. Куда положить
+
+Файл `.env` в корне `packages/server/` (в git не попадает — уже в .gitignore):
+
+```env
+TWITCH_CLIENT_ID=...
+TWITCH_CLIENT_SECRET=...
+TWITCH_CHANNEL=имя_канала
+
+TWITCH_BOT_TOKEN=...
+TWITCH_BOT_REFRESH=...
+
+TWITCH_STREAMER_TOKEN=...
+TWITCH_STREAMER_REFRESH=...
+```
+
+Refresh-токены обновляет `RefreshingAuthProvider` из twurple автоматически —
+перелогиниваться вручную не придётся.
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..114e3e9
--- /dev/null
+++ b/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "idle-xboct",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "packageManager": "pnpm@11.20.0",
+ "engines": {
+ "node": ">=22"
+ },
+ "scripts": {
+ "dev": "pnpm -r --parallel --stream dev",
+ "build": "pnpm -r build",
+ "typecheck": "pnpm -r typecheck"
+ }
+}
diff --git a/packages/overlay/.env.development b/packages/overlay/.env.development
new file mode 100644
index 0000000..82be29d
--- /dev/null
+++ b/packages/overlay/.env.development
@@ -0,0 +1 @@
+VITE_WS_URL=ws://localhost:3000/ws
diff --git a/packages/overlay/index.html b/packages/overlay/index.html
new file mode 100644
index 0000000..6d02cb3
--- /dev/null
+++ b/packages/overlay/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Idle XBOCT — overlay
+
+
+
+
+
+
diff --git a/packages/overlay/package.json b/packages/overlay/package.json
new file mode 100644
index 0000000..e81151a
--- /dev/null
+++ b/packages/overlay/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@idle/overlay",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@idle/render": "workspace:*",
+ "@idle/shared": "workspace:*"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^6.0.0",
+ "svelte": "^5.0.0",
+ "typescript": "^5.6.0",
+ "vite": "^7.0.0"
+ }
+}
diff --git a/packages/overlay/src/App.svelte b/packages/overlay/src/App.svelte
new file mode 100644
index 0000000..59d1c93
--- /dev/null
+++ b/packages/overlay/src/App.svelte
@@ -0,0 +1,63 @@
+
+
+
+
+
{STATUS_TEXT[status]}
+
+
+
diff --git a/packages/overlay/src/app.css b/packages/overlay/src/app.css
new file mode 100644
index 0000000..dc359f8
--- /dev/null
+++ b/packages/overlay/src/app.css
@@ -0,0 +1,9 @@
+html,
+body,
+#app {
+ margin: 0;
+ height: 100%;
+ /* прозрачность обязательна: канвас лежит поверх картинки стрима в OBS */
+ background: transparent;
+ overflow: hidden;
+}
diff --git a/packages/overlay/src/main.ts b/packages/overlay/src/main.ts
new file mode 100644
index 0000000..dd41642
--- /dev/null
+++ b/packages/overlay/src/main.ts
@@ -0,0 +1,5 @@
+import { mount } from 'svelte';
+import './app.css';
+import App from './App.svelte';
+
+export default mount(App, { target: document.getElementById('app')! });
diff --git a/packages/overlay/src/net.ts b/packages/overlay/src/net.ts
new file mode 100644
index 0000000..432b187
--- /dev/null
+++ b/packages/overlay/src/net.ts
@@ -0,0 +1,61 @@
+import { PROTOCOL_VERSION, type ClientMessage, type ServerMessage } from '@idle/shared';
+
+export type Snapshot = Extract;
+export type NetStatus = 'connecting' | 'online' | 'reconnecting';
+
+export function wsUrl(): string {
+ const fromEnv = import.meta.env.VITE_WS_URL;
+ if (typeof fromEnv === 'string' && fromEnv.length > 0) return fromEnv;
+ // сборка раздаётся самим сервером — тот же origin
+ const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
+ return `${scheme}://${location.host}/ws`;
+}
+
+export interface ConnectHandlers {
+ onStatus: (status: NetStatus) => void;
+ onSnapshot: (snap: Snapshot) => void;
+}
+
+/** Подключение к миру с автопереподключением; возвращает функцию закрытия. */
+export function connectWorld(handlers: ConnectHandlers): () => void {
+ let ws: WebSocket | null = null;
+ let disposed = false;
+ let attempt = 0;
+
+ const open = (): void => {
+ if (disposed) return;
+ handlers.onStatus(attempt === 0 ? 'connecting' : 'reconnecting');
+ ws = new WebSocket(wsUrl());
+
+ ws.onopen = () => {
+ attempt = 0;
+ const hello: ClientMessage = { t: 'hello', v: PROTOCOL_VERSION };
+ ws?.send(JSON.stringify(hello));
+ };
+ ws.onmessage = (ev) => {
+ let msg: ServerMessage;
+ try {
+ msg = JSON.parse(String(ev.data)) as ServerMessage;
+ } catch {
+ return;
+ }
+ if (msg.t === 'welcome') {
+ handlers.onStatus('online');
+ } else if (msg.t === 'snapshot') {
+ handlers.onSnapshot(msg);
+ }
+ };
+ ws.onclose = () => {
+ if (disposed) return;
+ attempt += 1;
+ setTimeout(open, Math.min(500 * 2 ** Math.min(attempt, 4), 8000));
+ };
+ ws.onerror = () => ws?.close();
+ };
+
+ open();
+ return () => {
+ disposed = true;
+ ws?.close();
+ };
+}
diff --git a/packages/overlay/src/vite-env.d.ts b/packages/overlay/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/packages/overlay/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/packages/overlay/tsconfig.json b/packages/overlay/tsconfig.json
new file mode 100644
index 0000000..474b75a
--- /dev/null
+++ b/packages/overlay/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "types": ["vite/client"]
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/packages/overlay/vite.config.ts b/packages/overlay/vite.config.ts
new file mode 100644
index 0000000..c01cd3e
--- /dev/null
+++ b/packages/overlay/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+
+export default defineConfig({
+ plugins: [svelte()],
+ server: { port: 5173, strictPort: true },
+});
diff --git a/packages/render/package.json b/packages/render/package.json
new file mode 100644
index 0000000..33469e9
--- /dev/null
+++ b/packages/render/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@idle/render",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ },
+ "scripts": {
+ "build": "tsc --noEmit",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@idle/shared": "workspace:*",
+ "pixi.js": "^8.6.0"
+ },
+ "devDependencies": {
+ "typescript": "^5.6.0"
+ }
+}
diff --git a/packages/render/src/avatar.ts b/packages/render/src/avatar.ts
new file mode 100644
index 0000000..0e8aa6e
--- /dev/null
+++ b/packages/render/src/avatar.ts
@@ -0,0 +1,58 @@
+import { Container, Graphics, Text } from 'pixi.js';
+import type { AvatarState } from '@idle/shared';
+
+/**
+ * Заглушка M0: человечек из примитивов. С M1 сюда придут спрайты из паков —
+ * состав (тело + ник над головой) сохранится.
+ */
+export class AvatarView {
+ readonly container = new Container();
+ private readonly rig = new Container();
+ private readonly label: Text;
+ private targetX: number;
+ private targetY: number;
+
+ constructor(av: AvatarState) {
+ const shadow = new Graphics().ellipse(0, 0, 9, 3.5).fill({ color: 0x000000, alpha: 0.3 });
+ const body = new Graphics()
+ .roundRect(-7, -17, 14, 17, 5)
+ .fill(av.color)
+ .circle(0, -23, 7.5)
+ .fill(0xffe3c0);
+ this.label = new Text({
+ text: av.name,
+ style: {
+ fontFamily: 'monospace',
+ fontSize: 13,
+ fontWeight: 'bold',
+ fill: av.color,
+ stroke: { color: 0x10131a, width: 3 },
+ },
+ });
+ this.label.anchor.set(0.5, 1);
+ this.label.y = -36;
+
+ this.rig.addChild(shadow, body);
+ this.container.addChild(this.rig, this.label);
+
+ this.targetX = av.x;
+ this.targetY = av.y;
+ this.container.position.set(av.x, av.y);
+ this.apply(av);
+ }
+
+ apply(av: AvatarState): void {
+ this.targetX = av.x;
+ this.targetY = av.y;
+ this.rig.scale.x = av.face === 'left' ? -1 : 1;
+ if (this.label.text !== av.name) this.label.text = av.name;
+ if (this.label.style.fill !== av.color) this.label.style.fill = av.color;
+ }
+
+ /** Плавное дотягивание до последнего снапшота — сглаживает дискретность тиков. */
+ tick(dtMs: number): void {
+ const k = 1 - Math.exp(-dtMs / 120);
+ this.container.x += (this.targetX - this.container.x) * k;
+ this.container.y += (this.targetY - this.container.y) * k;
+ }
+}
diff --git a/packages/render/src/index.ts b/packages/render/src/index.ts
new file mode 100644
index 0000000..2a21a42
--- /dev/null
+++ b/packages/render/src/index.ts
@@ -0,0 +1,2 @@
+export * from './avatar';
+export * from './world';
diff --git a/packages/render/src/world.ts b/packages/render/src/world.ts
new file mode 100644
index 0000000..dd4cb95
--- /dev/null
+++ b/packages/render/src/world.ts
@@ -0,0 +1,64 @@
+import { Application, Container } from 'pixi.js';
+import type { AvatarState } from '@idle/shared';
+import { AvatarView } from './avatar';
+
+export type WorldStatus = 'connecting' | 'online' | 'reconnecting';
+
+export interface MountWorldOptions {
+ /** 0 — прозрачный фон (overlay для OBS). */
+ backgroundAlpha?: number;
+ background?: number;
+ onStatus?: (status: WorldStatus) => void;
+}
+
+export interface WorldHandle {
+ applySnapshot(avatars: AvatarState[]): void;
+ destroy(): void;
+}
+
+/** Инициализация pixi-канваса в хосте; состояние мира придёт через applySnapshot. */
+export async function mountWorld(host: HTMLElement, opts: MountWorldOptions = {}): Promise {
+ opts.onStatus?.('connecting');
+
+ const app = new Application();
+ await app.init({
+ backgroundAlpha: opts.backgroundAlpha ?? 1,
+ background: opts.background ?? 0x151a21,
+ resizeTo: host,
+ antialias: true,
+ });
+ host.appendChild(app.canvas);
+ app.canvas.style.display = 'block';
+
+ const world = new Container();
+ app.stage.addChild(world);
+
+ const views = new Map();
+
+ app.ticker.add((ticker) => {
+ for (const view of views.values()) view.tick(ticker.deltaMS);
+ });
+
+ return {
+ applySnapshot(avatars) {
+ for (const av of avatars) {
+ let view = views.get(av.id);
+ if (!view) {
+ view = new AvatarView(av);
+ views.set(av.id, view);
+ world.addChild(view.container);
+ }
+ view.apply(av);
+ }
+ for (const [id, view] of views) {
+ if (!avatars.some((av) => av.id === id)) {
+ view.container.destroy();
+ views.delete(id);
+ }
+ }
+ },
+ destroy() {
+ void app.destroy(true, { children: true });
+ },
+ };
+}
diff --git a/packages/render/tsconfig.json b/packages/render/tsconfig.json
new file mode 100644
index 0000000..f200f34
--- /dev/null
+++ b/packages/render/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]
+ },
+ "include": ["src"]
+}
diff --git a/packages/server/package.json b/packages/server/package.json
new file mode 100644
index 0000000..4485f89
--- /dev/null
+++ b/packages/server/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@idle/server",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "tsx watch src/index.ts",
+ "start": "tsx src/index.ts",
+ "build": "tsc --noEmit",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@idle/shared": "workspace:*",
+ "sirv": "^3.0.0",
+ "ws": "^8.18.0"
+ },
+ "devDependencies": {
+ "@types/node": "^24.0.0",
+ "@types/ws": "^8.5.0",
+ "tsx": "^4.20.0",
+ "typescript": "^5.6.0"
+ }
+}
diff --git a/packages/server/smoke.mjs b/packages/server/smoke.mjs
new file mode 100644
index 0000000..62c00fc
--- /dev/null
+++ b/packages/server/smoke.mjs
@@ -0,0 +1,38 @@
+/* Дымовой тест M0: сервер должен быть запущен. `pnpm --filter @idle/server smoke` */
+import WebSocket from 'ws';
+
+const seen = [];
+await new Promise((resolve, reject) => {
+ const ws = new WebSocket('ws://localhost:3000/ws');
+ ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', v: 1 })));
+ ws.on('message', (d) => {
+ seen.push(JSON.parse(String(d)));
+ if (seen.length >= 2) {
+ ws.close();
+ resolve();
+ }
+ });
+ ws.on('error', reject);
+ setTimeout(() => reject(new Error('timeout waiting for welcome+snapshot')), 5000);
+});
+
+const [welcome, snap] = seen;
+console.log('welcome:', JSON.stringify(welcome));
+console.log(
+ 'avatars:',
+ snap.avatars.map((a) => `${a.name}@${a.x},${a.y} ${a.face}`).join(' | '),
+);
+if (welcome.t !== 'welcome' || welcome.v !== 1) throw new Error('bad welcome');
+if (snap.t !== 'snapshot' || snap.avatars.length !== 3) throw new Error('bad snapshot');
+
+// неверная версия протокола — сервер обязан закрыть сокет с кодом 4001
+const closeCode = await new Promise((resolve) => {
+ const ws = new WebSocket('ws://localhost:3000/ws');
+ ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', v: 999 })));
+ ws.on('close', (code) => resolve(code));
+ setTimeout(() => resolve('no-close'), 3000);
+});
+console.log('close code for wrong version:', closeCode);
+if (closeCode !== 4001) throw new Error(`expected close 4001, got ${closeCode}`);
+
+console.log('WS smoke: OK');
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
new file mode 100644
index 0000000..7300923
--- /dev/null
+++ b/packages/server/src/index.ts
@@ -0,0 +1,93 @@
+import { createServer } from 'node:http';
+import path from 'node:path';
+import { existsSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import type { IncomingMessage, ServerResponse } from 'node:http';
+import sirv from 'sirv';
+import { PROTOCOL_VERSION, type ServerMessage } from '@idle/shared';
+import { WsGateway } from './net/ws';
+import { stubAvatarsAt } from './sim/stub';
+
+const PORT = Number(process.env.PORT ?? 3000);
+const TICK_MS = Number(process.env.TICK_MS ?? 1000);
+const startedAt = Date.now();
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+const currentTick = (): number => Math.floor((Date.now() - startedAt) / TICK_MS);
+
+const snapshot = (): ServerMessage => ({
+ t: 'snapshot',
+ tick: currentTick(),
+ avatars: stubAvatarsAt(Date.now()),
+});
+
+const gateway = new WsGateway({ getSnapshot: snapshot, tickMs: TICK_MS });
+
+// Собранные клиенты раздаём сами — OBS обращается к одному порту.
+// В dev клиенты живут на своих vite-портах (5173/5174).
+const staticMounts = [
+ { prefix: '/overlay/', dir: path.resolve(__dirname, '../../overlay/dist') },
+ { prefix: '/web/', dir: path.resolve(__dirname, '../../web/dist') },
+].map((m) => ({ ...m, middleware: existsSync(m.dir) ? sirv(m.dir) : null }));
+
+function json(res: ServerResponse, status: number, body: unknown): void {
+ res.writeHead(status, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(body));
+}
+
+function handleHttp(req: IncomingMessage, res: ServerResponse): void {
+ const url = new URL(req.url ?? '/', 'http://localhost');
+ const p = url.pathname;
+
+ if (p === '/healthz') {
+ json(res, 200, {
+ ok: true,
+ protocol: PROTOCOL_VERSION,
+ tickMs: TICK_MS,
+ uptimeSec: Math.round((Date.now() - startedAt) / 1000),
+ });
+ return;
+ }
+
+ for (const m of staticMounts) {
+ if (!m.middleware) continue;
+ const base = m.prefix.slice(0, -1);
+ if (p === base) {
+ res.writeHead(301, { location: `${base}/` });
+ res.end();
+ return;
+ }
+ if (p.startsWith(m.prefix)) {
+ req.url = p.slice(base.length) + url.search;
+ m.middleware(req, res, () => json(res, 404, { error: 'not found' }));
+ return;
+ }
+ }
+
+ json(res, 404, {
+ error: 'not found',
+ hint: 'dev: overlay http://localhost:5173, web http://localhost:5174; для /overlay/ и /web/ сначала pnpm build',
+ });
+}
+
+const server = createServer(handleHttp);
+
+server.on('upgrade', (req, socket, head) => {
+ const { pathname } = new URL(req.url ?? '/', 'http://localhost');
+ if (pathname === '/ws') {
+ gateway.handleUpgrade(req, socket, head);
+ } else {
+ socket.destroy();
+ }
+});
+
+setInterval(() => gateway.broadcast(snapshot()), TICK_MS);
+
+server.listen(PORT, () => {
+ console.log(`[idle-xboct] http://localhost:${PORT} — протокол v${PROTOCOL_VERSION}, тик ${TICK_MS} мс`);
+ console.log(`[idle-xboct] overlay: http://localhost:5173 (dev) | web: http://localhost:5174 (dev)`);
+ const built = staticMounts.filter((m) => m.middleware).map((m) => m.prefix);
+ if (built.length > 0) {
+ console.log(`[idle-xboct] раздаю сборки: ${built.join(', ')}`);
+ }
+});
diff --git a/packages/server/src/net/ws.ts b/packages/server/src/net/ws.ts
new file mode 100644
index 0000000..bea5982
--- /dev/null
+++ b/packages/server/src/net/ws.ts
@@ -0,0 +1,91 @@
+import { WebSocketServer, WebSocket } from 'ws';
+import type { Duplex } from 'node:stream';
+import type { IncomingMessage } from 'node:http';
+import { PROTOCOL_VERSION, type ClientMessage, type ServerMessage } from '@idle/shared';
+
+declare module 'ws' {
+ interface WebSocket {
+ isAlive?: boolean;
+ }
+}
+
+interface GatewayOptions {
+ getSnapshot: () => ServerMessage;
+ tickMs: number;
+}
+
+/**
+ * Шлюз WS: рукопожатие по версии протокола, welcome + снапшот новому клиенту,
+ * broadcast снапшота каждый тик, heartbeat для мёртвых OBS-сокетов.
+ */
+export class WsGateway {
+ private readonly wss = new WebSocketServer({ noServer: true });
+ private readonly clients = new Set();
+
+ constructor(private readonly opts: GatewayOptions) {
+ this.wss.on('connection', (ws) => this.onConnection(ws));
+ setInterval(() => this.sweep(), 30_000).unref();
+ }
+
+ handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void {
+ this.wss.handleUpgrade(req, socket, head, (ws) => this.wss.emit('connection', ws, req));
+ }
+
+ broadcast(msg: ServerMessage): void {
+ const payload = JSON.stringify(msg);
+ for (const ws of this.clients) {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(payload);
+ }
+ }
+ }
+
+ private onConnection(ws: WebSocket): void {
+ ws.isAlive = true;
+ ws.on('pong', () => {
+ ws.isAlive = true;
+ });
+
+ let helloSeen = false;
+ ws.on('message', (raw) => {
+ if (helloSeen) return;
+ helloSeen = true;
+
+ let msg: ClientMessage | null = null;
+ try {
+ msg = JSON.parse(String(raw)) as ClientMessage;
+ } catch {
+ msg = null;
+ }
+ if (!msg || msg.t !== 'hello' || msg.v !== PROTOCOL_VERSION) {
+ ws.close(4001, 'protocol version mismatch');
+ return;
+ }
+
+ this.clients.add(ws);
+ this.send(ws, { t: 'welcome', v: PROTOCOL_VERSION, tickMs: this.opts.tickMs });
+ this.send(ws, this.opts.getSnapshot());
+ });
+
+ ws.on('close', () => this.clients.delete(ws));
+ ws.on('error', () => ws.terminate());
+ }
+
+ private send(ws: WebSocket, msg: ServerMessage): void {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify(msg));
+ }
+ }
+
+ private sweep(): void {
+ for (const ws of this.clients) {
+ if (ws.isAlive === false) {
+ this.clients.delete(ws);
+ ws.terminate();
+ continue;
+ }
+ ws.isAlive = false;
+ ws.ping();
+ }
+ }
+}
diff --git a/packages/server/src/sim/stub.ts b/packages/server/src/sim/stub.ts
new file mode 100644
index 0000000..120b1dc
--- /dev/null
+++ b/packages/server/src/sim/stub.ts
@@ -0,0 +1,43 @@
+import type { AvatarState } from '@idle/shared';
+
+interface Walker {
+ id: string;
+ name: string;
+ color: string;
+ y: number;
+ minX: number;
+ maxX: number;
+ /** px/сек */
+ speed: number;
+ /** сдвиг фазы 0..1, чтобы ходоки не шли строем */
+ offset: number;
+}
+
+const WALKERS: Walker[] = [
+ { id: 'stub-1', name: 'XBOCT', color: '#ff8c3b', y: 300, minX: 140, maxX: 820, speed: 90, offset: 0 },
+ { id: 'stub-2', name: 'pixel_proger', color: '#5bd7ff', y: 360, minX: 220, maxX: 900, speed: 70, offset: 0.35 },
+ { id: 'stub-3', name: 'viewer_42', color: '#b58cff', y: 240, minX: 80, maxX: 700, speed: 110, offset: 0.7 },
+];
+
+/**
+ * Заглушка M0: движение — чистая функция wall-clock.
+ * Сервер мог «не жить» сколько угодно — позиция восстановится из таймстемпа,
+ * как и задумано для ретроактивной симуляции.
+ */
+export function stubAvatarsAt(nowMs: number): AvatarState[] {
+ return WALKERS.map((w) => {
+ const length = w.maxX - w.minX;
+ const period = (2 * length) / w.speed;
+ const phase = (((nowMs / 1000) / period) + w.offset) % 1;
+ const leg = phase < 0.5 ? phase * 2 : 2 - phase * 2;
+ return {
+ id: w.id,
+ name: w.name,
+ color: w.color,
+ x: Math.round(w.minX + length * leg),
+ y: w.y,
+ face: phase < 0.5 ? 'right' : 'left',
+ moving: true,
+ } satisfies AvatarState;
+ });
+}
diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json
new file mode 100644
index 0000000..bdb9327
--- /dev/null
+++ b/packages/server/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "lib": ["ES2022"],
+ "types": ["node"]
+ },
+ "include": ["src"]
+}
diff --git a/packages/shared/package.json b/packages/shared/package.json
new file mode 100644
index 0000000..a906eaa
--- /dev/null
+++ b/packages/shared/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@idle/shared",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ },
+ "scripts": {
+ "build": "tsc --noEmit",
+ "typecheck": "tsc --noEmit"
+ },
+ "devDependencies": {
+ "typescript": "^5.6.0"
+ }
+}
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
new file mode 100644
index 0000000..995ea19
--- /dev/null
+++ b/packages/shared/src/index.ts
@@ -0,0 +1 @@
+export * from './protocol';
diff --git a/packages/shared/src/protocol.ts b/packages/shared/src/protocol.ts
new file mode 100644
index 0000000..ac223dd
--- /dev/null
+++ b/packages/shared/src/protocol.ts
@@ -0,0 +1,41 @@
+/**
+ * Протокол WS — контракт между сервером и клиентами (overlay / web).
+ * Клиенты зависят только от него; язык сервера можно сменить позже.
+ * Версия проверяется в рукопожатии (hello/welcome), чтобы OBS со старым
+ * бандлом не висел молча.
+ */
+
+export const PROTOCOL_VERSION = 1;
+
+export interface AvatarState {
+ id: string;
+ name: string;
+ /** Цвет ника из тегов чата Twitch. */
+ color: string;
+ /** Позиция в мировых координатах, px. */
+ x: number;
+ y: number;
+ face: 'left' | 'right';
+ moving: boolean;
+}
+
+export interface WelcomeMsg {
+ t: 'welcome';
+ v: number;
+ /** Период тика симуляции, мс — клиенту для отрисовки прогрессов. */
+ tickMs: number;
+}
+
+/** M0: полный снапшот каждый тик; дельты появятся с M1. */
+export interface SnapshotMsg {
+ t: 'snapshot';
+ tick: number;
+ avatars: AvatarState[];
+}
+
+export type ServerMessage = WelcomeMsg | SnapshotMsg;
+
+export type ClientMessage = {
+ t: 'hello';
+ v: number;
+};
diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json
new file mode 100644
index 0000000..48633a9
--- /dev/null
+++ b/packages/shared/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "lib": ["ES2022"]
+ },
+ "include": ["src"]
+}
diff --git a/packages/web/.env.development b/packages/web/.env.development
new file mode 100644
index 0000000..82be29d
--- /dev/null
+++ b/packages/web/.env.development
@@ -0,0 +1 @@
+VITE_WS_URL=ws://localhost:3000/ws
diff --git a/packages/web/index.html b/packages/web/index.html
new file mode 100644
index 0000000..afb5a81
--- /dev/null
+++ b/packages/web/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Idle XBOCT — мир
+
+
+
+
+
+
diff --git a/packages/web/package.json b/packages/web/package.json
new file mode 100644
index 0000000..bb70844
--- /dev/null
+++ b/packages/web/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@idle/web",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@idle/render": "workspace:*",
+ "@idle/shared": "workspace:*"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^6.0.0",
+ "svelte": "^5.0.0",
+ "typescript": "^5.6.0",
+ "vite": "^7.0.0"
+ }
+}
diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte
new file mode 100644
index 0000000..7cd4196
--- /dev/null
+++ b/packages/web/src/App.svelte
@@ -0,0 +1,125 @@
+
+
+
+
+ Idle XBOCT
+ {STATUS_TEXT[status]}
+
+
+
+
+
+
diff --git a/packages/web/src/app.css b/packages/web/src/app.css
new file mode 100644
index 0000000..67b748b
--- /dev/null
+++ b/packages/web/src/app.css
@@ -0,0 +1,13 @@
+:root {
+ color-scheme: dark;
+ font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
+}
+
+html,
+body,
+#app {
+ margin: 0;
+ height: 100%;
+ background: #151a21;
+ color: #dfe7f0;
+}
diff --git a/packages/web/src/main.ts b/packages/web/src/main.ts
new file mode 100644
index 0000000..dd41642
--- /dev/null
+++ b/packages/web/src/main.ts
@@ -0,0 +1,5 @@
+import { mount } from 'svelte';
+import './app.css';
+import App from './App.svelte';
+
+export default mount(App, { target: document.getElementById('app')! });
diff --git a/packages/web/src/net.ts b/packages/web/src/net.ts
new file mode 100644
index 0000000..432b187
--- /dev/null
+++ b/packages/web/src/net.ts
@@ -0,0 +1,61 @@
+import { PROTOCOL_VERSION, type ClientMessage, type ServerMessage } from '@idle/shared';
+
+export type Snapshot = Extract;
+export type NetStatus = 'connecting' | 'online' | 'reconnecting';
+
+export function wsUrl(): string {
+ const fromEnv = import.meta.env.VITE_WS_URL;
+ if (typeof fromEnv === 'string' && fromEnv.length > 0) return fromEnv;
+ // сборка раздаётся самим сервером — тот же origin
+ const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
+ return `${scheme}://${location.host}/ws`;
+}
+
+export interface ConnectHandlers {
+ onStatus: (status: NetStatus) => void;
+ onSnapshot: (snap: Snapshot) => void;
+}
+
+/** Подключение к миру с автопереподключением; возвращает функцию закрытия. */
+export function connectWorld(handlers: ConnectHandlers): () => void {
+ let ws: WebSocket | null = null;
+ let disposed = false;
+ let attempt = 0;
+
+ const open = (): void => {
+ if (disposed) return;
+ handlers.onStatus(attempt === 0 ? 'connecting' : 'reconnecting');
+ ws = new WebSocket(wsUrl());
+
+ ws.onopen = () => {
+ attempt = 0;
+ const hello: ClientMessage = { t: 'hello', v: PROTOCOL_VERSION };
+ ws?.send(JSON.stringify(hello));
+ };
+ ws.onmessage = (ev) => {
+ let msg: ServerMessage;
+ try {
+ msg = JSON.parse(String(ev.data)) as ServerMessage;
+ } catch {
+ return;
+ }
+ if (msg.t === 'welcome') {
+ handlers.onStatus('online');
+ } else if (msg.t === 'snapshot') {
+ handlers.onSnapshot(msg);
+ }
+ };
+ ws.onclose = () => {
+ if (disposed) return;
+ attempt += 1;
+ setTimeout(open, Math.min(500 * 2 ** Math.min(attempt, 4), 8000));
+ };
+ ws.onerror = () => ws?.close();
+ };
+
+ open();
+ return () => {
+ disposed = true;
+ ws?.close();
+ };
+}
diff --git a/packages/web/src/vite-env.d.ts b/packages/web/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/packages/web/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json
new file mode 100644
index 0000000..474b75a
--- /dev/null
+++ b/packages/web/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "types": ["vite/client"]
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts
new file mode 100644
index 0000000..65f598a
--- /dev/null
+++ b/packages/web/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+
+export default defineConfig({
+ plugins: [svelte()],
+ server: { port: 5174, strictPort: true },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..d4350b5
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,1099 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .: {}
+
+ packages/overlay:
+ dependencies:
+ '@idle/render':
+ specifier: workspace:*
+ version: link:../render
+ '@idle/shared':
+ specifier: workspace:*
+ version: link:../shared
+ devDependencies:
+ '@sveltejs/vite-plugin-svelte':
+ specifier: ^6.0.0
+ version: 6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13))
+ svelte:
+ specifier: ^5.0.0
+ version: 5.57.0
+ typescript:
+ specifier: ^5.6.0
+ version: 5.9.3
+ vite:
+ specifier: ^7.0.0
+ version: 7.3.6(@types/node@24.13.3)(tsx@4.23.13)
+
+ packages/render:
+ dependencies:
+ '@idle/shared':
+ specifier: workspace:*
+ version: link:../shared
+ pixi.js:
+ specifier: ^8.6.0
+ version: 8.20.1
+ devDependencies:
+ typescript:
+ specifier: ^5.6.0
+ version: 5.9.3
+
+ packages/server:
+ dependencies:
+ '@idle/shared':
+ specifier: workspace:*
+ version: link:../shared
+ sirv:
+ specifier: ^3.0.0
+ version: 3.0.2
+ ws:
+ specifier: ^8.18.0
+ version: 8.21.3
+ devDependencies:
+ '@types/node':
+ specifier: ^24.0.0
+ version: 24.13.3
+ '@types/ws':
+ specifier: ^8.5.0
+ version: 8.18.1
+ tsx:
+ specifier: ^4.20.0
+ version: 4.23.13
+ typescript:
+ specifier: ^5.6.0
+ version: 5.9.3
+
+ packages/shared:
+ devDependencies:
+ typescript:
+ specifier: ^5.6.0
+ version: 5.9.3
+
+ packages/web:
+ dependencies:
+ '@idle/render':
+ specifier: workspace:*
+ version: link:../render
+ '@idle/shared':
+ specifier: workspace:*
+ version: link:../shared
+ devDependencies:
+ '@sveltejs/vite-plugin-svelte':
+ specifier: ^6.0.0
+ version: 6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13))
+ svelte:
+ specifier: ^5.0.0
+ version: 5.57.0
+ typescript:
+ specifier: ^5.6.0
+ version: 5.9.3
+ vite:
+ specifier: ^7.0.0
+ version: 7.3.6(@types/node@24.13.3)(tsx@4.23.13)
+
+packages:
+
+ '@esbuild/aix-ppc64@0.28.2':
+ resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.28.2':
+ resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.28.2':
+ resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.28.2':
+ resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.28.2':
+ resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.28.2':
+ resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.28.2':
+ resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.28.2':
+ resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.28.2':
+ resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.28.2':
+ resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.28.2':
+ resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.28.2':
+ resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.28.2':
+ resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.28.2':
+ resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.28.2':
+ resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.28.2':
+ resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.28.2':
+ resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.28.2':
+ resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.28.2':
+ resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.28.2':
+ resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.28.2':
+ resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.28.2':
+ resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.28.2':
+ resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.28.2':
+ resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
+ engines: {node: ^22.20 || ^24.12 || >=25}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pixi/colord@2.9.6':
+ resolution: {integrity: sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==}
+
+ '@polka/url@1.0.0-next.29':
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
+ '@rollup/rollup-android-arm-eabi@4.63.1':
+ resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.63.1':
+ resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.63.1':
+ resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.63.1':
+ resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.63.1':
+ resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.63.1':
+ resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.63.1':
+ resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.63.1':
+ resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-arm64-gnu@4.63.1':
+ resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm64-musl@4.63.1':
+ resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-loong64-gnu@4.63.1':
+ resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-loong64-musl@4.63.1':
+ resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.63.1':
+ resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-ppc64-musl@4.63.1':
+ resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.63.1':
+ resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-musl@4.63.1':
+ resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-s390x-gnu@4.63.1':
+ resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-gnu@4.63.1':
+ resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-musl@4.63.1':
+ resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-openbsd-x64@4.63.1':
+ resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.63.1':
+ resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.63.1':
+ resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.63.1':
+ resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.63.1':
+ resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.63.1':
+ resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==}
+ cpu: [x64]
+ os: [win32]
+
+ '@sveltejs/acorn-typescript@1.0.13':
+ resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==}
+ peerDependencies:
+ acorn: ^8.9.0
+
+ '@sveltejs/vite-plugin-svelte-inspector@5.0.2':
+ resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==}
+ engines: {node: ^20.19 || ^22.12 || >=24}
+ peerDependencies:
+ '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0
+ svelte: ^5.0.0
+ vite: ^6.3.0 || ^7.0.0
+
+ '@sveltejs/vite-plugin-svelte@6.2.4':
+ resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==}
+ engines: {node: ^20.19 || ^22.12 || >=24}
+ peerDependencies:
+ svelte: ^5.0.0
+ vite: ^6.3.0 || ^7.0.0
+
+ '@types/earcut@3.0.0':
+ resolution: {integrity: sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/node@24.13.3':
+ resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
+
+ '@types/ws@8.18.1':
+ resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
+
+ '@webgpu/types@0.1.72':
+ resolution: {integrity: sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw==}
+
+ '@xmldom/xmldom@0.8.15':
+ resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==}
+ engines: {node: '>=10.0.0'}
+
+ acorn@8.18.0:
+ resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ aria-query@5.3.1:
+ resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
+ engines: {node: '>= 0.4'}
+
+ axobject-query@4.1.0:
+ resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
+ engines: {node: '>= 0.4'}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
+ devalue@5.9.2:
+ resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==}
+
+ earcut@3.2.3:
+ resolution: {integrity: sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==}
+
+ esbuild@0.28.2:
+ resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ esm-env@1.2.2:
+ resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
+
+ esrap@2.3.6:
+ resolution: {integrity: sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==}
+ peerDependencies:
+ '@typescript-eslint/types': ^8.2.0
+ peerDependenciesMeta:
+ '@typescript-eslint/types':
+ optional: true
+
+ eventemitter3@5.0.4:
+ resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ gifuct-js@2.1.2:
+ resolution: {integrity: sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==}
+
+ is-reference@3.0.3:
+ resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
+
+ ismobilejs@1.1.1:
+ resolution: {integrity: sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==}
+
+ js-binary-schema-parser@2.0.3:
+ resolution: {integrity: sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==}
+
+ locate-character@3.0.0:
+ resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
+ nanoid@3.3.18:
+ resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ obug@2.1.4:
+ resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
+ engines: {node: '>=12.20.0'}
+
+ parse-svg-path@0.2.0:
+ resolution: {integrity: sha512-Tf7FFIrguPKQwzD4pWnYkR2VOv3raoHeKED80Bm+BYHI3KxC8KsgsGC5+fSMzAGDA6UEk4bHvmi+RsjmL3khpg==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.7:
+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
+ engines: {node: '>=12'}
+
+ pixi.js@8.20.1:
+ resolution: {integrity: sha512-akLVBMLvQbaEViqsmVraK0Njer1q4Q4lm4iNKuzvBiFrV8q+6/+HluJN3sWIwO663IBeQtUUS/CaXFJZs6ffXQ==}
+
+ postcss@8.5.26:
+ resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ rollup@4.63.1:
+ resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ sirv@3.0.2:
+ resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
+ engines: {node: '>=18'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ svelte@5.57.0:
+ resolution: {integrity: sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==}
+ engines: {node: '>=18'}
+
+ tiny-lru@11.4.7:
+ resolution: {integrity: sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==}
+ engines: {node: '>=12'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
+ tsx@4.23.13:
+ resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.18.2:
+ resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+
+ vite@7.3.6:
+ resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitefu@1.1.3:
+ resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
+ ws@8.21.3:
+ resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ zimmerframe@1.1.5:
+ resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==}
+
+snapshots:
+
+ '@esbuild/aix-ppc64@0.28.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/android-arm@0.28.2':
+ optional: true
+
+ '@esbuild/android-x64@0.28.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.28.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.28.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.28.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.28.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.28.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.28.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.28.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.28.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.28.2':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.6.0': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ optional: true
+
+ '@pixi/colord@2.9.6': {}
+
+ '@polka/url@1.0.0-next.29': {}
+
+ '@rollup/rollup-android-arm-eabi@4.63.1':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.63.1':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.63.1':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.63.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.63.1':
+ optional: true
+
+ '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)':
+ dependencies:
+ acorn: 8.18.0
+
+ '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)))(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13))':
+ dependencies:
+ '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13))
+ obug: 2.1.4
+ svelte: 5.57.0
+ vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.13)
+
+ '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13))':
+ dependencies:
+ '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)))(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13))
+ deepmerge: 4.3.1
+ magic-string: 0.30.21
+ obug: 2.1.4
+ svelte: 5.57.0
+ vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.13)
+ vitefu: 1.1.3(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13))
+
+ '@types/earcut@3.0.0': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/node@24.13.3':
+ dependencies:
+ undici-types: 7.18.2
+
+ '@types/ws@8.18.1':
+ dependencies:
+ '@types/node': 24.13.3
+
+ '@webgpu/types@0.1.72': {}
+
+ '@xmldom/xmldom@0.8.15': {}
+
+ acorn@8.18.0: {}
+
+ aria-query@5.3.1: {}
+
+ axobject-query@4.1.0: {}
+
+ clsx@2.1.1: {}
+
+ deepmerge@4.3.1: {}
+
+ devalue@5.9.2: {}
+
+ earcut@3.2.3: {}
+
+ esbuild@0.28.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.28.2
+ '@esbuild/android-arm': 0.28.2
+ '@esbuild/android-arm64': 0.28.2
+ '@esbuild/android-x64': 0.28.2
+ '@esbuild/darwin-arm64': 0.28.2
+ '@esbuild/darwin-x64': 0.28.2
+ '@esbuild/freebsd-arm64': 0.28.2
+ '@esbuild/freebsd-x64': 0.28.2
+ '@esbuild/linux-arm': 0.28.2
+ '@esbuild/linux-arm64': 0.28.2
+ '@esbuild/linux-ia32': 0.28.2
+ '@esbuild/linux-loong64': 0.28.2
+ '@esbuild/linux-mips64el': 0.28.2
+ '@esbuild/linux-ppc64': 0.28.2
+ '@esbuild/linux-riscv64': 0.28.2
+ '@esbuild/linux-s390x': 0.28.2
+ '@esbuild/linux-x64': 0.28.2
+ '@esbuild/netbsd-arm64': 0.28.2
+ '@esbuild/netbsd-x64': 0.28.2
+ '@esbuild/openbsd-arm64': 0.28.2
+ '@esbuild/openbsd-x64': 0.28.2
+ '@esbuild/openharmony-arm64': 0.28.2
+ '@esbuild/sunos-x64': 0.28.2
+ '@esbuild/win32-arm64': 0.28.2
+ '@esbuild/win32-ia32': 0.28.2
+ '@esbuild/win32-x64': 0.28.2
+
+ esm-env@1.2.2: {}
+
+ esrap@2.3.6:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ eventemitter3@5.0.4: {}
+
+ fdir@6.5.0(picomatch@4.0.7):
+ optionalDependencies:
+ picomatch: 4.0.7
+
+ fsevents@2.3.3:
+ optional: true
+
+ gifuct-js@2.1.2:
+ dependencies:
+ js-binary-schema-parser: 2.0.3
+
+ is-reference@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ ismobilejs@1.1.1: {}
+
+ js-binary-schema-parser@2.0.3: {}
+
+ locate-character@3.0.0: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ mrmime@2.0.1: {}
+
+ nanoid@3.3.18: {}
+
+ obug@2.1.4: {}
+
+ parse-svg-path@0.2.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.7: {}
+
+ pixi.js@8.20.1:
+ dependencies:
+ '@pixi/colord': 2.9.6
+ '@types/earcut': 3.0.0
+ '@webgpu/types': 0.1.72
+ '@xmldom/xmldom': 0.8.15
+ earcut: 3.2.3
+ eventemitter3: 5.0.4
+ gifuct-js: 2.1.2
+ ismobilejs: 1.1.1
+ parse-svg-path: 0.2.0
+ tiny-lru: 11.4.7
+
+ postcss@8.5.26:
+ dependencies:
+ nanoid: 3.3.18
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ rollup@4.63.1:
+ dependencies:
+ '@types/estree': 1.0.9
+ optionalDependencies:
+ '@napi-rs/lzma-linux-x64-gnu': 1.5.1
+ '@rollup/rollup-android-arm-eabi': 4.63.1
+ '@rollup/rollup-android-arm64': 4.63.1
+ '@rollup/rollup-darwin-arm64': 4.63.1
+ '@rollup/rollup-darwin-x64': 4.63.1
+ '@rollup/rollup-freebsd-arm64': 4.63.1
+ '@rollup/rollup-freebsd-x64': 4.63.1
+ '@rollup/rollup-linux-arm-gnueabihf': 4.63.1
+ '@rollup/rollup-linux-arm-musleabihf': 4.63.1
+ '@rollup/rollup-linux-arm64-gnu': 4.63.1
+ '@rollup/rollup-linux-arm64-musl': 4.63.1
+ '@rollup/rollup-linux-loong64-gnu': 4.63.1
+ '@rollup/rollup-linux-loong64-musl': 4.63.1
+ '@rollup/rollup-linux-ppc64-gnu': 4.63.1
+ '@rollup/rollup-linux-ppc64-musl': 4.63.1
+ '@rollup/rollup-linux-riscv64-gnu': 4.63.1
+ '@rollup/rollup-linux-riscv64-musl': 4.63.1
+ '@rollup/rollup-linux-s390x-gnu': 4.63.1
+ '@rollup/rollup-linux-x64-gnu': 4.63.1
+ '@rollup/rollup-linux-x64-musl': 4.63.1
+ '@rollup/rollup-openbsd-x64': 4.63.1
+ '@rollup/rollup-openharmony-arm64': 4.63.1
+ '@rollup/rollup-win32-arm64-msvc': 4.63.1
+ '@rollup/rollup-win32-ia32-msvc': 4.63.1
+ '@rollup/rollup-win32-x64-gnu': 4.63.1
+ '@rollup/rollup-win32-x64-msvc': 4.63.1
+ fsevents: 2.3.3
+
+ sirv@3.0.2:
+ dependencies:
+ '@polka/url': 1.0.0-next.29
+ mrmime: 2.0.1
+ totalist: 3.0.1
+
+ source-map-js@1.2.1: {}
+
+ svelte@5.57.0:
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ '@jridgewell/sourcemap-codec': 1.6.0
+ '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0)
+ '@types/estree': 1.0.9
+ acorn: 8.18.0
+ aria-query: 5.3.1
+ axobject-query: 4.1.0
+ clsx: 2.1.1
+ devalue: 5.9.2
+ esm-env: 1.2.2
+ esrap: 2.3.6
+ is-reference: 3.0.3
+ locate-character: 3.0.0
+ magic-string: 0.30.21
+ zimmerframe: 1.1.5
+ transitivePeerDependencies:
+ - '@typescript-eslint/types'
+
+ tiny-lru@11.4.7: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+
+ totalist@3.0.1: {}
+
+ tsx@4.23.13:
+ dependencies:
+ esbuild: 0.28.2
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ typescript@5.9.3: {}
+
+ undici-types@7.18.2: {}
+
+ vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13):
+ dependencies:
+ esbuild: 0.28.2
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+ postcss: 8.5.26
+ rollup: 4.63.1
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 24.13.3
+ fsevents: 2.3.3
+ tsx: 4.23.13
+
+ vitefu@1.1.3(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)):
+ optionalDependencies:
+ vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.13)
+
+ ws@8.21.3: {}
+
+ zimmerframe@1.1.5: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..8f4884f
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,4 @@
+packages:
+ - packages/*
+allowBuilds:
+ esbuild: false
diff --git a/tsconfig.base.json b/tsconfig.base.json
new file mode 100644
index 0000000..a5f06e6
--- /dev/null
+++ b/tsconfig.base.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "verbatimModuleSyntax": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ "noEmit": true
+ }
+}