feat: 集成 Tailwind CSS v4,替换手写样式为 utility classes

This commit is contained in:
2026-03-02 06:18:20 +08:00
parent aaea414d5a
commit ea46ad71bf
7 changed files with 311 additions and 424 deletions

View File

@@ -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 {

View File

@@ -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": {

View File

@@ -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=="],
}
}

View File

@@ -8,50 +8,50 @@
<meta name="theme-color" content="#08080d">
<title>VoicePaste</title>
</head>
<body>
<div id="app">
<header>
<h1>VoicePaste</h1>
<div id="status" class="status disconnected">
<span class="dot"></span>
<body class="h-full bg-bg text-fg overflow-hidden select-none">
<div id="app" class="relative z-1 flex flex-col h-full max-w-[480px] mx-auto">
<header class="flex items-center justify-between pt-2 pb-5 shrink-0">
<h1 class="text-[22px] font-bold tracking-[-0.03em]">VoicePaste</h1>
<div id="status" class="status disconnected flex items-center gap-[7px] text-xs font-medium text-fg-dim px-3 py-[5px] rounded-full bg-surface border border-edge transition-all">
<span class="dot size-[7px] rounded-full bg-fg-dim shrink-0 transition-all duration-300"></span>
<span id="status-text">连接中…</span>
</div>
</header>
<section id="preview-section">
<div id="preview" class="preview-box">
<p id="preview-text" class="placeholder">按住说话…</p>
<section id="preview-section" class="shrink-0 pb-3">
<div id="preview" class="preview-box bg-surface border border-edge rounded-card px-[18px] py-4 min-h-20 max-h-40 overflow-y-auto transition-all duration-300">
<p id="preview-text" class="placeholder text-base leading-relaxed break-words">按住说话…</p>
</div>
</section>
<section id="mic-section">
<div class="mic-wrapper">
<button id="mic-btn" type="button" disabled>
<section id="mic-section" class="flex flex-col items-center pt-5 pb-4 shrink-0 gap-3.5">
<div class="mic-wrapper relative flex items-center justify-center touch-none">
<button id="mic-btn" class="relative z-1 size-24 rounded-full border-2 border-edge text-fg-secondary flex items-center justify-center cursor-pointer select-none touch-none" type="button" disabled>
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor" aria-label="麦克风" role="img">
<title>麦克风</title>
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
</svg>
</button>
<div class="mic-rings" aria-hidden="true">
<span class="ring"></span>
<span class="ring"></span>
<span class="ring"></span>
<div class="mic-rings absolute inset-0 pointer-events-none" aria-hidden="true">
<span class="ring absolute inset-0 rounded-full border-[1.5px] border-accent opacity-0"></span>
<span class="ring absolute inset-0 rounded-full border-[1.5px] border-accent opacity-0"></span>
<span class="ring absolute inset-0 rounded-full border-[1.5px] border-accent opacity-0"></span>
</div>
</div>
<p class="mic-hint">按住说话</p>
<p class="text-fg-dim text-sm font-medium">按住说话</p>
</section>
<section id="history-section">
<div class="history-header">
<h2>历史记录</h2>
<button id="clear-history" type="button" class="text-btn">清空</button>
<section id="history-section" class="flex-1 min-h-0 flex flex-col overflow-hidden">
<div class="flex items-center justify-between pb-2.5 shrink-0">
<h2 class="text-[13px] font-semibold text-fg-dim uppercase tracking-[0.06em]">历史记录</h2>
<button id="clear-history" type="button" class="text-btn bg-transparent border-none text-fg-dim text-xs font-medium cursor-pointer px-2.5 py-1 rounded-lg transition-all duration-150">清空</button>
</div>
<ul id="history-list"></ul>
<p id="history-empty" class="placeholder">暂无记录</p>
<ul id="history-list" class="list-none flex-1 overflow-y-auto"></ul>
<p id="history-empty" class="text-fg-dim text-sm text-center py-10">暂无记录</p>
</section>
</div>
<div id="toast" class="toast"></div>
<div id="toast" class="toast fixed left-1/2 z-[999] rounded-3xl text-[13px] font-medium text-fg border border-edge pointer-events-none opacity-0 px-[22px] py-2.5"></div>
<script type="module" src="app.ts"></script>
</body>

View File

@@ -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"
}

View File

@@ -1,54 +1,38 @@
*,
*::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%;
/* ─── Base ─── */
@layer base {
html,
body {
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 {
body::after {
content: "";
position: fixed;
@apply fixed pointer-events-none z-0;
top: -30%;
left: 50%;
transform: translateX(-50%);
@@ -59,78 +43,149 @@ body::after {
rgba(99, 102, 241, 0.04) 0%,
transparent 70%
);
pointer-events: none;
z-index: 0;
}
}
/* ─── 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);
}

View File

@@ -1,6 +1,8 @@
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss()],
root: ".",
build: {
outDir: "dist",