feat: setup project

This commit is contained in:
2026-09-05 15:27:53 +05:00
parent ae2a7e1ce9
commit c79681dc59
43 changed files with 2194 additions and 14 deletions
+1
View File
@@ -0,0 +1 @@
VITE_WS_URL=ws://localhost:3000/ws
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Idle XBOCT — overlay</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+21
View File
@@ -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"
}
}
+63
View File
@@ -0,0 +1,63 @@
<script lang="ts">
import { onMount } from 'svelte';
import { mountWorld, type WorldHandle } from '@idle/render';
import { connectWorld, type ConnectHandlers, type NetStatus } from './net';
const STATUS_TEXT: Record<NetStatus, string> = {
connecting: 'подключение…',
online: 'онлайн',
reconnecting: 'переподключение…',
};
let host = $state<HTMLDivElement | undefined>(undefined);
let status = $state<NetStatus>('connecting');
onMount(() => {
let world: WorldHandle | undefined;
let disposeNet: (() => void) | undefined;
if (host) {
void mountWorld(host, { backgroundAlpha: 0 }).then((w) => {
world = w;
const handlers: ConnectHandlers = {
onStatus: (s) => {
status = s;
},
onSnapshot: (snap) => world?.applySnapshot(snap.avatars),
};
disposeNet = connectWorld(handlers);
});
}
return () => {
disposeNet?.();
world?.destroy();
};
});
</script>
<div class="root">
<div class="world" bind:this={host}></div>
<div class="badge">{STATUS_TEXT[status]}</div>
</div>
<style>
.root {
position: fixed;
inset: 0;
}
.world {
position: absolute;
inset: 0;
}
.badge {
position: absolute;
top: 8px;
right: 8px;
font: 12px/1.4 monospace;
color: #9fb0c3;
background: rgba(16, 19, 26, 0.55);
padding: 2px 8px;
border-radius: 8px;
}
</style>
+9
View File
@@ -0,0 +1,9 @@
html,
body,
#app {
margin: 0;
height: 100%;
/* прозрачность обязательна: канвас лежит поверх картинки стрима в OBS */
background: transparent;
overflow: hidden;
}
+5
View File
@@ -0,0 +1,5 @@
import { mount } from 'svelte';
import './app.css';
import App from './App.svelte';
export default mount(App, { target: document.getElementById('app')! });
+61
View File
@@ -0,0 +1,61 @@
import { PROTOCOL_VERSION, type ClientMessage, type ServerMessage } from '@idle/shared';
export type Snapshot = Extract<ServerMessage, { t: 'snapshot' }>;
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();
};
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"]
},
"include": ["src", "vite.config.ts"]
}
+7
View File
@@ -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 },
});