diff --git a/web/app.ts b/web/app.ts index 12c2372..0618667 100644 --- a/web/app.ts +++ b/web/app.ts @@ -113,7 +113,8 @@ function wsUrl(): string { return `${proto}//${location.host}/ws${q}`; } function setStatus(cls: string, text: string): void { - statusEl.className = `status ${cls}`; + statusEl.classList.remove("connected", "disconnected", "connecting"); + statusEl.classList.add(cls); statusText.textContent = text; } function connectWS(): void { diff --git a/web/biome.json b/web/biome.json index b9b52b1..3b56d18 100644 --- a/web/biome.json +++ b/web/biome.json @@ -15,7 +15,21 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "nursery": { + "useSortedClasses": "warn" + } + } + }, + "css": { + "parser": { + "tailwindDirectives": true + }, + "formatter": { + "enabled": true + }, + "linter": { + "enabled": true } }, "javascript": { diff --git a/web/bun.lock b/web/bun.lock index fd1e39c..37c5a76 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -6,6 +6,8 @@ "name": "web", "devDependencies": { "@biomejs/biome": "^2.4.4", + "@tailwindcss/vite": "^4.2.1", + "tailwindcss": "^4.2.1", "typescript": "^5.9.3", "vite": "^7.3.1", }, @@ -82,6 +84,16 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], @@ -132,16 +144,80 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -154,6 +230,10 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -161,5 +241,17 @@ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], } } diff --git a/web/index.html b/web/index.html index fd51816..8ebd46e 100644 --- a/web/index.html +++ b/web/index.html @@ -8,50 +8,50 @@ VoicePaste - -
-
-

VoicePaste

-
- + +
+
+

VoicePaste

+
+ 连接中…
-
-
-

按住说话…

+
+
+

按住说话…

-
-
- - -

按住说话

+

按住说话

-
-
-

历史记录

- +
+
+

历史记录

+
-
    -

    暂无记录

    +
      +

      暂无记录

      -
      +
      diff --git a/web/package.json b/web/package.json index 002cbbb..1d577e5 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,8 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.4", + "@tailwindcss/vite": "^4.2.1", + "tailwindcss": "^4.2.1", "typescript": "^5.9.3", "vite": "^7.3.1" } diff --git a/web/style.css b/web/style.css index b786332..326ab80 100644 --- a/web/style.css +++ b/web/style.css @@ -1,136 +1,191 @@ -*, -*::before, -*::after { - margin: 0; - padding: 0; - box-sizing: border-box; +@import "tailwindcss"; + +/* ─── Design Tokens ─── */ +@theme { + --color-bg: #08080d; + --color-surface: #111117; + --color-surface-hover: #17171e; + --color-surface-active: #1c1c25; + --color-edge: #1e1e2a; + --color-edge-active: #2c2c3e; + --color-fg: #eaeaef; + --color-fg-secondary: #9e9eb5; + --color-fg-dim: #5a5a6e; + --color-accent: #6366f1; + --color-accent-hover: #818cf8; + --color-danger: #f43f5e; + --color-success: #34d399; + --radius-card: 14px; } -:root { - /* Warm dark palette — subtle indigo undertone */ - --bg: #08080d; - --surface: #111117; - --surface-hover: #17171e; - --surface-active: #1c1c25; - --border: #1e1e2a; - --border-active: #2c2c3e; - --text: #eaeaef; - --text-secondary: #9e9eb5; - --text-dim: #5a5a6e; - --accent: #6366f1; - --accent-hover: #818cf8; - --accent-glow: rgba(99, 102, 241, 0.2); - --accent-glow-md: rgba(99, 102, 241, 0.35); - --accent-glow-lg: rgba(99, 102, 241, 0.08); - --danger: #f43f5e; - --success: #34d399; - --radius: 14px; - --radius-sm: 8px; - --safe-top: env(safe-area-inset-top, 0px); - --safe-bottom: env(safe-area-inset-bottom, 0px); -} - -html, -body { - height: 100%; - font-family: - "SF Pro Display", -apple-system, BlinkMacSystemFont, "PingFang SC", - "Hiragino Sans GB", "Microsoft YaHei", sans-serif; - background: var(--bg); - color: var(--text); - -webkit-font-smoothing: antialiased; - -webkit-tap-highlight-color: transparent; - -webkit-touch-callout: none; - user-select: none; - overflow: hidden; -} - -/* Subtle ambient glow at top */ -body::after { - content: ""; - position: fixed; - top: -30%; - left: 50%; - transform: translateX(-50%); - width: 600px; - height: 600px; - background: radial-gradient( - circle, - rgba(99, 102, 241, 0.04) 0%, - transparent 70% - ); - pointer-events: none; - z-index: 0; +/* ─── Base ─── */ +@layer base { + html, + body { + font-family: + "SF Pro Display", -apple-system, BlinkMacSystemFont, "PingFang SC", + "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + } + + body::after { + content: ""; + @apply fixed pointer-events-none z-0; + top: -30%; + left: 50%; + transform: translateX(-50%); + width: 600px; + height: 600px; + background: radial-gradient( + circle, + rgba(99, 102, 241, 0.04) 0%, + transparent 70% + ); + } } +/* ─── App Container (safe-area padding) ─── */ #app { - position: relative; - z-index: 1; - display: flex; - flex-direction: column; - height: 100%; - max-width: 480px; - margin: 0 auto; - padding: calc(16px + var(--safe-top)) 20px calc(16px + var(--safe-bottom)); -} - -/* ─── Header ─── */ -header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 0 20px; - flex-shrink: 0; -} - -header h1 { - font-size: 22px; - font-weight: 700; - letter-spacing: -0.03em; -} - -.status { - display: flex; - align-items: center; - gap: 7px; - font-size: 12px; - font-weight: 500; - color: var(--text-dim); - padding: 5px 12px; - border-radius: 20px; - background: var(--surface); - border: 1px solid var(--border); - transition: all 0.25s ease; -} - -.status .dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--text-dim); - transition: all 0.3s ease; - flex-shrink: 0; + padding: calc(16px + env(safe-area-inset-top, 0px)) 20px + calc(16px + env(safe-area-inset-bottom, 0px)); } +/* ─── Connection Status States ─── */ .status.connected { - border-color: rgba(52, 211, 153, 0.15); + @apply border-success/15; } - .status.connected .dot { - background: var(--success); + @apply bg-success; box-shadow: 0 0 6px rgba(52, 211, 153, 0.5); } - .status.disconnected .dot { - background: var(--danger); + @apply bg-danger; box-shadow: 0 0 6px rgba(244, 63, 94, 0.4); } - .status.connecting .dot { - background: var(--accent); + @apply bg-accent; animation: pulse 1.4s ease-in-out infinite; } +/* ─── Preview Active State ─── */ +.preview-box.active { + border-color: rgba(99, 102, 241, 0.4); + box-shadow: + 0 0 0 1px rgba(99, 102, 241, 0.2), + 0 4px 24px -4px rgba(99, 102, 241, 0.15); + background: linear-gradient( + 180deg, + rgba(99, 102, 241, 0.03) 0%, + var(--color-surface) 100% + ); +} + +/* ─── Placeholder Text ─── */ +#preview-text.placeholder { + @apply text-fg-dim; +} + +/* ─── Mic Button ─── */ +#mic-btn { + background: linear-gradient( + 145deg, + var(--color-surface-hover), + var(--color-surface) + ); + box-shadow: + 0 2px 12px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.04); + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} +#mic-btn:disabled { + @apply opacity-30 cursor-not-allowed; +} +#mic-btn:not(:disabled):active, +#mic-btn.recording { + @apply bg-accent border-accent-hover text-white; + transform: scale(1.06); + box-shadow: + 0 0 32px rgba(99, 102, 241, 0.35), + 0 0 80px rgba(99, 102, 241, 0.2); +} +#mic-btn.recording { + animation: mic-breathe 1.8s ease-in-out infinite; +} + +/* ─── Wave Rings ─── */ +#mic-btn.recording + .mic-rings .ring { + animation: ring-expand 2.4s cubic-bezier(0.2, 0, 0.2, 1) infinite; +} +#mic-btn.recording + .mic-rings .ring:nth-child(2) { + animation-delay: 0.8s; +} +#mic-btn.recording + .mic-rings .ring:nth-child(3) { + animation-delay: 1.6s; +} + +/* ─── History Items (dynamically created) ─── */ +#history-list li { + @apply bg-surface border border-edge rounded-card cursor-pointer flex items-start gap-3 transition-all duration-150; + padding: 14px 16px; + margin-bottom: 8px; + font-size: 14px; + line-height: 1.5; + animation: slide-up 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; + animation-delay: calc(var(--i, 0) * 40ms); +} +#history-list li:active { + @apply bg-surface-active border-edge-active; + transform: scale(0.985); +} +#history-list li .hist-text { + @apply flex-1 break-words; +} +#history-list li .hist-time { + @apply text-fg-dim whitespace-nowrap shrink-0; + font-size: 11px; + padding-top: 2px; + font-variant-numeric: tabular-nums; +} + +/* ─── Text Button Active State ─── */ +.text-btn:active { + @apply text-danger; + background: rgba(244, 63, 94, 0.08); +} + +/* ─── Scrollbar ─── */ +.preview-box::-webkit-scrollbar, +#history-list::-webkit-scrollbar { + width: 3px; +} +.preview-box::-webkit-scrollbar-track, +#history-list::-webkit-scrollbar-track { + background: transparent; +} +.preview-box::-webkit-scrollbar-thumb, +#history-list::-webkit-scrollbar-thumb { + @apply bg-edge; + border-radius: 3px; +} + +/* ─── Toast ─── */ +.toast { + bottom: calc(80px + env(safe-area-inset-bottom, 0px)); + transform: translateX(-50%) translateY(8px); + background: rgba(28, 28, 37, 0.85); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} +.toast.show { + @apply opacity-100; + transform: translateX(-50%) translateY(0); +} + +/* ─── Keyframes ─── */ @keyframes pulse { 0%, 100% { @@ -141,153 +196,21 @@ header h1 { } } -/* ─── Preview ─── */ -#preview-section { - flex-shrink: 0; - padding-bottom: 12px; -} - -.preview-box { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px 18px; - min-height: 80px; - max-height: 160px; - overflow-y: auto; - transition: - border-color 0.3s ease, - box-shadow 0.3s ease, - background 0.3s ease; -} - -.preview-box.active { - border-color: rgba(99, 102, 241, 0.4); - box-shadow: - 0 0 0 1px var(--accent-glow), - 0 4px 24px -4px rgba(99, 102, 241, 0.15); - background: linear-gradient( - 180deg, - rgba(99, 102, 241, 0.03) 0%, - var(--surface) 100% - ); -} - -#preview-text { - font-size: 16px; - line-height: 1.6; - word-break: break-word; -} - -#preview-text.placeholder { - color: var(--text-dim); -} - -/* ─── Mic Button ─── */ -#mic-section { - display: flex; - flex-direction: column; - align-items: center; - padding: 20px 0 16px; - flex-shrink: 0; - gap: 14px; -} - -.mic-wrapper { - position: relative; - display: flex; - align-items: center; - justify-content: center; - touch-action: none; -} - -#mic-btn { - position: relative; - z-index: 1; - width: 96px; - height: 96px; - border-radius: 50%; - border: 2px solid var(--border); - background: linear-gradient(145deg, var(--surface-hover), var(--surface)); - color: var(--text-secondary); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); - -webkit-user-select: none; - touch-action: none; - box-shadow: - 0 2px 12px rgba(0, 0, 0, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.04); -} - -#mic-btn svg { - transition: transform 0.2s ease; -} - -#mic-btn:disabled { - opacity: 0.3; - cursor: not-allowed; -} - -#mic-btn:not(:disabled):active, -#mic-btn.recording { - background: var(--accent); - border-color: var(--accent-hover); - color: #fff; - transform: scale(1.06); - box-shadow: - 0 0 32px var(--accent-glow-md), - 0 0 80px var(--accent-glow); -} - -#mic-btn.recording { - animation: mic-breathe 1.8s ease-in-out infinite; -} - @keyframes mic-breathe { 0%, 100% { box-shadow: - 0 0 32px var(--accent-glow-md), - 0 0 80px var(--accent-glow); + 0 0 32px rgba(99, 102, 241, 0.35), + 0 0 80px rgba(99, 102, 241, 0.2); } 50% { box-shadow: - 0 0 48px var(--accent-glow-md), - 0 0 120px var(--accent-glow), - 0 0 200px var(--accent-glow-lg); + 0 0 48px rgba(99, 102, 241, 0.35), + 0 0 120px rgba(99, 102, 241, 0.2), + 0 0 200px rgba(99, 102, 241, 0.08); } } -/* Wave rings — radiate outward when recording */ -.mic-rings { - position: absolute; - inset: 0; - pointer-events: none; -} - -.mic-rings .ring { - position: absolute; - inset: 0; - border-radius: 50%; - border: 1.5px solid var(--accent); - opacity: 0; -} - -#mic-btn.recording + .mic-rings .ring { - animation: ring-expand 2.4s cubic-bezier(0.2, 0, 0.2, 1) infinite; -} - -#mic-btn.recording + .mic-rings .ring:nth-child(2) { - animation-delay: 0.8s; -} - -#mic-btn.recording + .mic-rings .ring:nth-child(3) { - animation-delay: 1.6s; -} - @keyframes ring-expand { 0% { transform: scale(1); @@ -299,99 +222,6 @@ header h1 { } } -.mic-hint { - font-size: 13px; - color: var(--text-dim); - letter-spacing: 0.01em; - transition: color 0.3s ease; -} - -/* ─── History ─── */ -#history-section { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.history-header { - display: flex; - align-items: center; - justify-content: space-between; - padding-bottom: 10px; - flex-shrink: 0; -} - -.history-header h2 { - font-size: 13px; - font-weight: 600; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.text-btn { - background: none; - border: none; - color: var(--text-dim); - font-size: 12px; - font-weight: 500; - cursor: pointer; - padding: 4px 10px; - border-radius: var(--radius-sm); - transition: all 0.15s ease; -} - -.text-btn:active { - color: var(--danger); - background: rgba(244, 63, 94, 0.08); -} - -#history-list { - list-style: none; - flex: 1; - overflow-y: auto; - -webkit-overflow-scrolling: touch; -} - -#history-list li { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 14px 16px; - margin-bottom: 8px; - font-size: 14px; - line-height: 1.5; - cursor: pointer; - transition: all 0.15s ease; - display: flex; - align-items: flex-start; - gap: 12px; - animation: slide-up 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; - animation-delay: calc(var(--i, 0) * 40ms); -} - -#history-list li:active { - background: var(--surface-active); - border-color: var(--border-active); - transform: scale(0.985); -} - -#history-list li .hist-text { - flex: 1; - word-break: break-word; -} - -#history-list li .hist-time { - font-size: 11px; - color: var(--text-dim); - white-space: nowrap; - flex-shrink: 0; - padding-top: 2px; - font-variant-numeric: tabular-nums; -} - @keyframes slide-up { from { opacity: 0; @@ -402,57 +232,3 @@ header h1 { transform: translateY(0); } } - -#history-empty { - text-align: center; - padding: 40px 0; -} - -.placeholder { - color: var(--text-dim); - font-size: 14px; -} - -/* ─── Scrollbar ─── */ -.preview-box::-webkit-scrollbar, -#history-list::-webkit-scrollbar { - width: 3px; -} - -.preview-box::-webkit-scrollbar-track, -#history-list::-webkit-scrollbar-track { - background: transparent; -} - -.preview-box::-webkit-scrollbar-thumb, -#history-list::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: 3px; -} - -/* ─── Toast ─── */ -.toast { - position: fixed; - bottom: calc(80px + var(--safe-bottom, 0px)); - left: 50%; - transform: translateX(-50%) translateY(8px); - background: rgba(28, 28, 37, 0.85); - color: var(--text); - padding: 10px 22px; - border-radius: 24px; - font-size: 13px; - font-weight: 500; - z-index: 999; - opacity: 0; - transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); - pointer-events: none; - border: 1px solid var(--border); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); -} - -.toast.show { - opacity: 1; - transform: translateX(-50%) translateY(0); -} diff --git a/web/vite.config.ts b/web/vite.config.ts index fc1374f..fd0fe68 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,6 +1,8 @@ +import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; export default defineConfig({ + plugins: [tailwindcss()], root: ".", build: { outDir: "dist",