commit 518410edd009f1cde099b68e28fbe6d1fc865aae Author: eric Date: Sun Sep 27 12:15:00 2026 -0500 feat: 游民导航 H5、微信小程序与小红书小工具 Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fcf5f38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Build / pack outputs +xhs/release/ +xhs/dist/ +*.zip + +# Local secrets / one-off assets +.env +.env.* +nomadro.png +scripts/_*.py + diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b26efe --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Navmini · 游民导航 + +移动端优先的 **Link 导航** H5,内容对齐 [nav.hackrobot.cn](https://nav.hackrobot.cn/)。效果认可后再做微信 / 抖音 / 小红书小程序。 + +## 本地预览 + +```bash +npm install +npm run dev +``` + +手机或浏览器窄屏预览最佳。 + +```bash +npm run build +npm run preview +``` + +## 能力 + +- 整页长滚动浏览全部入口(图标 + 标题 + 简介) +- 左侧抽屉可快速跳到某一分类 +- 顶栏随滚动显示当前分类 +- 浅色 / 深色主题记忆 +- 回到顶部 + +## 图标 + +图标已离线压缩到 [`public/icons/`](public/icons/)(96×96 WebP)。`links.json` 引用 `/icons/{id}.webp`。 + +重新拉取并压缩: + +```bash +npm run icons +``` + + +编辑 [`src/data/links.json`](src/data/links.json): + +``` +categories[] → groups[] → links[] + id / title / subtitle / href / icon / action? +``` + +`action` 默认 `open`;设为 `copy` 时写入剪贴板。 + +## 小程序 + +- [`xhs/`](xhs/) 小红书小工具(离线导航,手动选中复制网址) +- [`weapp/`](weapp/) 微信小程序(抽屉 + 长列表,一键复制网址) + +```bash +npm run weapp:sync +``` + +微信开发者工具打开 `weapp/` 目录。详见 [`weapp/README.md`](weapp/README.md)。 + +- `tt/` 抖音小程序(待做) diff --git a/index.html b/index.html new file mode 100644 index 0000000..7c038d2 --- /dev/null +++ b/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + 游民导航 + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9154b6e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2188 @@ +{ + "name": "navmini", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "navmini", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "icojs": "^1.0.1", + "oxlint": "^1.81.0", + "sharp": "^0.35.5", + "typescript": "~6.0.2", + "vite": "^8.3.0" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@canvas/image-data": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.1.0.tgz", + "integrity": "sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.85.0.tgz", + "integrity": "sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.85.0.tgz", + "integrity": "sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.85.0.tgz", + "integrity": "sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.85.0.tgz", + "integrity": "sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.85.0.tgz", + "integrity": "sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.85.0.tgz", + "integrity": "sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.85.0.tgz", + "integrity": "sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.85.0.tgz", + "integrity": "sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.85.0.tgz", + "integrity": "sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.85.0.tgz", + "integrity": "sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.85.0.tgz", + "integrity": "sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.85.0.tgz", + "integrity": "sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.85.0.tgz", + "integrity": "sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.85.0.tgz", + "integrity": "sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.85.0.tgz", + "integrity": "sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.85.0.tgz", + "integrity": "sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.85.0.tgz", + "integrity": "sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.85.0.tgz", + "integrity": "sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.85.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.0", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/bmp-ts": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz", + "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-bmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/decode-bmp/-/decode-bmp-0.2.1.tgz", + "integrity": "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0", + "to-data-view": "^1.1.0" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/decode-ico": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-ico/-/decode-ico-0.4.1.tgz", + "integrity": "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@canvas/image-data": "^1.0.0", + "decode-bmp": "^0.2.0", + "to-data-view": "^1.1.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-type": { + "version": "22.1.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.1.1.tgz", + "integrity": "sha512-sEB0oStmeLGtpmSAD88RDNuLsvhv4iC3u0lE25CUvfCULvj49xoQ3l3K525Vn+ZzsZ6DOLajQCV6RmhwwhqiJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/icojs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/icojs/-/icojs-1.0.1.tgz", + "integrity": "sha512-6WG7MNK0kPsiX1zqilHX3plEVtGKEqojox5Of69YtIZH+0cPns+Cxab2MLrJSJabpfleQrQ9KqWLcs62P1pDfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bmp-ts": "^1.0.9", + "decode-ico": "^0.4.1", + "file-type": "^22.0.2", + "jpeg-js": "^0.4.4", + "pngjs": "^7.0.0" + }, + "engines": { + "node": ">=22.22.2" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.85.0", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.85.0", + "@oxlint/binding-android-arm64": "1.85.0", + "@oxlint/binding-darwin-arm64": "1.85.0", + "@oxlint/binding-darwin-x64": "1.85.0", + "@oxlint/binding-freebsd-x64": "1.85.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", + "@oxlint/binding-linux-arm-musleabihf": "1.85.0", + "@oxlint/binding-linux-arm64-gnu": "1.85.0", + "@oxlint/binding-linux-arm64-musl": "1.85.0", + "@oxlint/binding-linux-ppc64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-musl": "1.85.0", + "@oxlint/binding-linux-s390x-gnu": "1.85.0", + "@oxlint/binding-linux-x64-gnu": "1.85.0", + "@oxlint/binding-linux-x64-musl": "1.85.0", + "@oxlint/binding-openharmony-arm64": "1.85.0", + "@oxlint/binding-win32-arm64-msvc": "1.85.0", + "@oxlint/binding-win32-ia32-msvc": "1.85.0", + "@oxlint/binding-win32-x64-msvc": "1.85.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.11", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-data-view": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", + "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8array-extras": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..81b3427 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "navmini", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview", + "icons": "node scripts/fetch-icons.mjs", + "xhs:assemble": "python xhs/scripts/assemble.py", + "xhs:pack": "python xhs/scripts/pack.py", + "xhs:build": "python xhs/scripts/assemble.py && python xhs/scripts/pack.py", + "weapp:sync": "python weapp/scripts/sync_data.py" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "icojs": "^1.0.1", + "oxlint": "^1.81.0", + "sharp": "^0.35.5", + "typescript": "~6.0.2", + "vite": "^8.3.0" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/1psim.webp b/public/icons/1psim.webp new file mode 100644 index 0000000..a64841a Binary files /dev/null and b/public/icons/1psim.webp differ diff --git a/public/icons/3x-ui.webp b/public/icons/3x-ui.webp new file mode 100644 index 0000000..6ef85ad Binary files /dev/null and b/public/icons/3x-ui.webp differ diff --git a/public/icons/9esim.webp b/public/icons/9esim.webp new file mode 100644 index 0000000..e260506 Binary files /dev/null and b/public/icons/9esim.webp differ diff --git a/public/icons/_brand.webp b/public/icons/_brand.webp new file mode 100644 index 0000000..9cf906b Binary files /dev/null and b/public/icons/_brand.webp differ diff --git a/public/icons/aapanel.webp b/public/icons/aapanel.webp new file mode 100644 index 0000000..75eabea Binary files /dev/null and b/public/icons/aapanel.webp differ diff --git a/public/icons/action-cam.webp b/public/icons/action-cam.webp new file mode 100644 index 0000000..91cf880 Binary files /dev/null and b/public/icons/action-cam.webp differ diff --git a/public/icons/adsense.webp b/public/icons/adsense.webp new file mode 100644 index 0000000..d6dcd29 Binary files /dev/null and b/public/icons/adsense.webp differ diff --git a/public/icons/ai-glasses.webp b/public/icons/ai-glasses.webp new file mode 100644 index 0000000..63c57a1 Binary files /dev/null and b/public/icons/ai-glasses.webp differ diff --git a/public/icons/aliyun.webp b/public/icons/aliyun.webp new file mode 100644 index 0000000..d6fac1a Binary files /dev/null and b/public/icons/aliyun.webp differ diff --git a/public/icons/bmc.webp b/public/icons/bmc.webp new file mode 100644 index 0000000..f151a5d Binary files /dev/null and b/public/icons/bmc.webp differ diff --git a/public/icons/bwh.webp b/public/icons/bwh.webp new file mode 100644 index 0000000..265aaa0 Binary files /dev/null and b/public/icons/bwh.webp differ diff --git a/public/icons/chatgpt.webp b/public/icons/chatgpt.webp new file mode 100644 index 0000000..4d86031 Binary files /dev/null and b/public/icons/chatgpt.webp differ diff --git a/public/icons/cloud-phone.webp b/public/icons/cloud-phone.webp new file mode 100644 index 0000000..34036ed Binary files /dev/null and b/public/icons/cloud-phone.webp differ diff --git a/public/icons/cloudflare.webp b/public/icons/cloudflare.webp new file mode 100644 index 0000000..8de0f5a Binary files /dev/null and b/public/icons/cloudflare.webp differ diff --git a/public/icons/colocrossing.webp b/public/icons/colocrossing.webp new file mode 100644 index 0000000..06dd41c Binary files /dev/null and b/public/icons/colocrossing.webp differ diff --git a/public/icons/creem.webp b/public/icons/creem.webp new file mode 100644 index 0000000..f8b751e Binary files /dev/null and b/public/icons/creem.webp differ diff --git a/public/icons/drone.webp b/public/icons/drone.webp new file mode 100644 index 0000000..edc61f8 Binary files /dev/null and b/public/icons/drone.webp differ diff --git a/public/icons/dsx.webp b/public/icons/dsx.webp new file mode 100644 index 0000000..de1e117 Binary files /dev/null and b/public/icons/dsx.webp differ diff --git a/public/icons/ffmpeg.webp b/public/icons/ffmpeg.webp new file mode 100644 index 0000000..8599db3 Binary files /dev/null and b/public/icons/ffmpeg.webp differ diff --git a/public/icons/frp.webp b/public/icons/frp.webp new file mode 100644 index 0000000..c6547fe Binary files /dev/null and b/public/icons/frp.webp differ diff --git a/public/icons/ga.webp b/public/icons/ga.webp new file mode 100644 index 0000000..4bc3288 Binary files /dev/null and b/public/icons/ga.webp differ diff --git a/public/icons/gpu.webp b/public/icons/gpu.webp new file mode 100644 index 0000000..4e0757e Binary files /dev/null and b/public/icons/gpu.webp differ diff --git a/public/icons/grok.webp b/public/icons/grok.webp new file mode 100644 index 0000000..2498a74 Binary files /dev/null and b/public/icons/grok.webp differ diff --git a/public/icons/iproyal.webp b/public/icons/iproyal.webp new file mode 100644 index 0000000..22747a1 Binary files /dev/null and b/public/icons/iproyal.webp differ diff --git a/public/icons/jms.webp b/public/icons/jms.webp new file mode 100644 index 0000000..ee44874 Binary files /dev/null and b/public/icons/jms.webp differ diff --git a/public/icons/lineageos.webp b/public/icons/lineageos.webp new file mode 100644 index 0000000..300499d Binary files /dev/null and b/public/icons/lineageos.webp differ diff --git a/public/icons/lisa.webp b/public/icons/lisa.webp new file mode 100644 index 0000000..ada3df6 Binary files /dev/null and b/public/icons/lisa.webp differ diff --git a/public/icons/mihomo.webp b/public/icons/mihomo.webp new file mode 100644 index 0000000..75708e5 Binary files /dev/null and b/public/icons/mihomo.webp differ diff --git a/public/icons/mini-pc.webp b/public/icons/mini-pc.webp new file mode 100644 index 0000000..f3038b0 Binary files /dev/null and b/public/icons/mini-pc.webp differ diff --git a/public/icons/namesilo.webp b/public/icons/namesilo.webp new file mode 100644 index 0000000..f29a994 Binary files /dev/null and b/public/icons/namesilo.webp differ diff --git a/public/icons/nomad-book.webp b/public/icons/nomad-book.webp new file mode 100644 index 0000000..b495988 Binary files /dev/null and b/public/icons/nomad-book.webp differ diff --git a/public/icons/nomadcna.webp b/public/icons/nomadcna.webp new file mode 100644 index 0000000..285782e Binary files /dev/null and b/public/icons/nomadcna.webp differ diff --git a/public/icons/nomadlist.webp b/public/icons/nomadlist.webp new file mode 100644 index 0000000..640d92f Binary files /dev/null and b/public/icons/nomadlist.webp differ diff --git a/public/icons/nomadro.webp b/public/icons/nomadro.webp new file mode 100644 index 0000000..39b692f Binary files /dev/null and b/public/icons/nomadro.webp differ diff --git a/public/icons/nowpayments.webp b/public/icons/nowpayments.webp new file mode 100644 index 0000000..e042076 Binary files /dev/null and b/public/icons/nowpayments.webp differ diff --git a/public/icons/obs.webp b/public/icons/obs.webp new file mode 100644 index 0000000..2fa929f Binary files /dev/null and b/public/icons/obs.webp differ diff --git a/public/icons/okx.webp b/public/icons/okx.webp new file mode 100644 index 0000000..85aa2e3 Binary files /dev/null and b/public/icons/okx.webp differ diff --git a/public/icons/paypal.webp b/public/icons/paypal.webp new file mode 100644 index 0000000..3cc416e Binary files /dev/null and b/public/icons/paypal.webp differ diff --git a/public/icons/proxy-cheap.webp b/public/icons/proxy-cheap.webp new file mode 100644 index 0000000..7483870 Binary files /dev/null and b/public/icons/proxy-cheap.webp differ diff --git a/public/icons/qcloud.webp b/public/icons/qcloud.webp new file mode 100644 index 0000000..53a5430 Binary files /dev/null and b/public/icons/qcloud.webp differ diff --git a/public/icons/racknerd.webp b/public/icons/racknerd.webp new file mode 100644 index 0000000..2262856 Binary files /dev/null and b/public/icons/racknerd.webp differ diff --git a/public/icons/rpi.webp b/public/icons/rpi.webp new file mode 100644 index 0000000..cc6268a Binary files /dev/null and b/public/icons/rpi.webp differ diff --git a/public/icons/safepal.webp b/public/icons/safepal.webp new file mode 100644 index 0000000..570274e Binary files /dev/null and b/public/icons/safepal.webp differ diff --git a/public/icons/smartproxy.webp b/public/icons/smartproxy.webp new file mode 100644 index 0000000..96ba0cf Binary files /dev/null and b/public/icons/smartproxy.webp differ diff --git a/public/icons/smmfollows.webp b/public/icons/smmfollows.webp new file mode 100644 index 0000000..7e39e92 Binary files /dev/null and b/public/icons/smmfollows.webp differ diff --git a/public/icons/srs.webp b/public/icons/srs.webp new file mode 100644 index 0000000..ee1c4a8 Binary files /dev/null and b/public/icons/srs.webp differ diff --git a/public/icons/stripe.webp b/public/icons/stripe.webp new file mode 100644 index 0000000..829ef26 Binary files /dev/null and b/public/icons/stripe.webp differ diff --git a/public/icons/tello.webp b/public/icons/tello.webp new file mode 100644 index 0000000..9ec43f3 Binary files /dev/null and b/public/icons/tello.webp differ diff --git a/public/icons/tiktok.webp b/public/icons/tiktok.webp new file mode 100644 index 0000000..d66705e Binary files /dev/null and b/public/icons/tiktok.webp differ diff --git a/public/icons/vcam.webp b/public/icons/vcam.webp new file mode 100644 index 0000000..66c1003 Binary files /dev/null and b/public/icons/vcam.webp differ diff --git a/public/icons/voyracloud.webp b/public/icons/voyracloud.webp new file mode 100644 index 0000000..077e0b5 Binary files /dev/null and b/public/icons/voyracloud.webp differ diff --git a/public/icons/vultr.webp b/public/icons/vultr.webp new file mode 100644 index 0000000..a6caed2 Binary files /dev/null and b/public/icons/vultr.webp differ diff --git a/public/icons/wireguard.webp b/public/icons/wireguard.webp new file mode 100644 index 0000000..df6acdd Binary files /dev/null and b/public/icons/wireguard.webp differ diff --git a/public/icons/wise.webp b/public/icons/wise.webp new file mode 100644 index 0000000..429ae7c Binary files /dev/null and b/public/icons/wise.webp differ diff --git a/public/icons/xorpay.webp b/public/icons/xorpay.webp new file mode 100644 index 0000000..b20e15a Binary files /dev/null and b/public/icons/xorpay.webp differ diff --git a/public/icons/yesim.webp b/public/icons/yesim.webp new file mode 100644 index 0000000..be7ae90 Binary files /dev/null and b/public/icons/yesim.webp differ diff --git a/public/icons/yidu.webp b/public/icons/yidu.webp new file mode 100644 index 0000000..4de6f0d Binary files /dev/null and b/public/icons/yidu.webp differ diff --git a/public/icons/ykk.webp b/public/icons/ykk.webp new file mode 100644 index 0000000..fb18929 Binary files /dev/null and b/public/icons/ykk.webp differ diff --git a/public/icons/youtube.webp b/public/icons/youtube.webp new file mode 100644 index 0000000..5ee5b72 Binary files /dev/null and b/public/icons/youtube.webp differ diff --git a/public/icons/yt-auto-dismiss.webp b/public/icons/yt-auto-dismiss.webp new file mode 100644 index 0000000..b575d7e Binary files /dev/null and b/public/icons/yt-auto-dismiss.webp differ diff --git a/public/icons/zpay.webp b/public/icons/zpay.webp new file mode 100644 index 0000000..3dbbd06 Binary files /dev/null and b/public/icons/zpay.webp differ diff --git a/scripts/fetch-icons.mjs b/scripts/fetch-icons.mjs new file mode 100644 index 0000000..42ae26c --- /dev/null +++ b/scripts/fetch-icons.mjs @@ -0,0 +1,166 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import sharp from 'sharp' +import { decodeIco, isIco } from 'icojs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const root = path.resolve(__dirname, '..') +const outDir = path.join(root, 'public', 'icons') +const linksPath = path.join(root, 'src', 'data', 'links.json') +const SIZE = 96 +const WEBP_QUALITY = 72 + +const SOURCE = { + nomadro: 'https://nav.hackrobot.cn/assets/images/nomadro2.png', + nomadlist: 'https://nav.hackrobot.cn/assets/images/nomadlist.png', + nomadcna: 'https://nav.hackrobot.cn/assets/images/nomadcna.jpg', + chatgpt: 'https://nav.hackrobot.cn/assets/images/logos/chatgpt.png', + grok: 'https://nav.hackrobot.cn/assets/favicon_1759046289851.png', + '1psim': 'https://nav.hackrobot.cn/assets/images/1psim.png', + yesim: 'https://nav.hackrobot.cn/assets/images/yesim.png', + tello: 'https://nav.hackrobot.cn/assets/images/tello.png', + '9esim': 'https://nav.hackrobot.cn/assets/images/9esim.png', + wise: 'https://nav.hackrobot.cn/assets/images/logos/wise.png', + okx: 'https://nav.hackrobot.cn/assets/images/logos/okx.png', + safepal: 'https://nav.hackrobot.cn/assets/images/logos/safepal.png', + paypal: 'https://nav.hackrobot.cn/assets/images/paypal.png', + bmc: 'https://nav.hackrobot.cn/assets/images/buymecoffee.png', + jms: 'https://nav.hackrobot.cn/assets/images/logos/proxy.png', + ykk: 'https://nav.hackrobot.cn/assets/images/logos/ykk.jpg', + vultr: 'https://nav.hackrobot.cn/assets/images/logos/vultr.png', + bwh: 'https://nav.hackrobot.cn/assets/images/logos/bandwagonhost.jpg', + racknerd: 'https://nav.hackrobot.cn/assets/images/logos/RackNerd.jpg', + qcloud: 'https://nav.hackrobot.cn/assets/images/tengxunyun.png', + aliyun: 'https://nav.hackrobot.cn/assets/images/aliyun.jpg', + colocrossing: 'https://nav.hackrobot.cn/assets/images/logos/colocrossing.png', + iproyal: 'https://nav.hackrobot.cn/assets/images/logos/iproyal.jpg', + 'proxy-cheap': 'https://nav.hackrobot.cn/assets/images/logos/proxy-cheap.jpg', + smartproxy: 'https://nav.hackrobot.cn/assets/images/logos/smartproxy.jpg', + lisa: 'https://nav.hackrobot.cn/assets/images/lisa.png', + voyracloud: 'https://nav.hackrobot.cn/assets/images/voyracloud.jpg', + rpi: 'https://nav.hackrobot.cn/assets/images/logos/raspberrypi.png', + 'mini-pc': 'https://nav.hackrobot.cn/assets/images/logos/lingke.jpg', + gpu: 'https://nav.hackrobot.cn/assets/images/logos/nvidia.png', + '3x-ui': 'https://nav.hackrobot.cn/assets/images/logos/3x.jpg', + mihomo: 'https://nav.hackrobot.cn/assets/images/mihomo.jpg', + frp: 'https://nav.hackrobot.cn/assets/images/logos/frp.png', + wireguard: 'https://nav.hackrobot.cn/assets/images/wireguard.png', + lineageos: 'https://nav.hackrobot.cn/assets/images/logos/lineageos.webp', + 'ai-glasses': 'https://nav.hackrobot.cn/assets/images/logos/glass.jpg', + 'action-cam': 'https://nav.hackrobot.cn/assets/images/logos/action.jpg', + drone: 'https://nav.hackrobot.cn/assets/images/logos/neo2.jpg', + tiktok: 'https://nav.hackrobot.cn/assets/images/logos/tiktok.png', + 'cloud-phone': 'https://nav.hackrobot.cn/assets/images/logos/phone.png', + vcam: 'https://nav.hackrobot.cn/assets/images/vcam.jpg', + youtube: 'https://nav.hackrobot.cn/assets/images/logos/youtube.png', + smmfollows: 'https://nav.hackrobot.cn/assets/images/logos/qqtube.png', + 'yt-auto-dismiss': 'https://nav.hackrobot.cn/assets/images/greasyfork.png', + namesilo: 'https://nav.hackrobot.cn/assets/images/logos/namesilo.jpg', + aapanel: 'https://nav.hackrobot.cn/assets/images/logos/aapanel.png', + cloudflare: 'https://nav.hackrobot.cn/assets/images/logos/cloudflar.ico', + ga: 'https://nav.hackrobot.cn/assets/images/logos/analytics.ico', + adsense: 'https://nav.hackrobot.cn/assets/images/logos/gads.ico', + obs: 'https://nav.hackrobot.cn/assets/images/obs.jpg', + ffmpeg: 'https://nav.hackrobot.cn/assets/images/ffmpeg.jpg', + srs: 'https://nav.hackrobot.cn/assets/images/srs.png', + nowpayments: 'https://nav.hackrobot.cn/assets/images/nowpayments.ico', + creem: 'https://nav.hackrobot.cn/assets/images/creem.ico', + stripe: 'https://nav.hackrobot.cn/assets/images/stripe.ico', + zpay: 'https://nav.hackrobot.cn/assets/images/zpay.ico', + xorpay: 'https://nav.hackrobot.cn/assets/images/xorpay.ico', + yidu: 'https://nav.hackrobot.cn/assets/images/blog.jpg', + dsx: 'https://nav.hackrobot.cn/assets/images/dsx.jpg', + _brand: 'https://nav.hackrobot.cn/assets/images/nomadro.png', +} + +async function fetchBuffer(url) { + const res = await fetch(url) + if (!res.ok) throw new Error(`${res.status} ${url}`) + return Buffer.from(await res.arrayBuffer()) +} + +async function normalizeInput(buf) { + if (isIco(buf)) { + const images = await decodeIco(buf) + let best = images[0] + let bestArea = 0 + for (const img of images) { + const meta = await sharp(Buffer.from(img.buffer), { failOn: 'none' }).metadata() + const area = (meta.width || 0) * (meta.height || 0) + if (area >= bestArea) { + bestArea = area + best = img + } + } + return Buffer.from(best.buffer) + } + return buf +} + +async function toWebp(buf) { + const input = await normalizeInput(buf) + return sharp(input, { animated: false, failOn: 'none' }) + .rotate() + .resize(SIZE, SIZE, { + fit: 'cover', + withoutEnlargement: false, + background: { r: 255, g: 255, b: 255, alpha: 0 }, + }) + .webp({ quality: WEBP_QUALITY, effort: 6 }) + .toBuffer() +} + +async function main() { + await mkdir(outDir, { recursive: true }) + const data = JSON.parse(await readFile(linksPath, 'utf8')) + + const ids = new Set(['_brand']) + for (const cat of data.categories) { + for (const group of cat.groups) { + for (const link of group.links) ids.add(link.id) + } + } + + let ok = 0 + let fail = 0 + let bytes = 0 + + for (const id of ids) { + const url = SOURCE[id] + if (!url) { + console.error(`✗ ${id}: missing source URL`) + fail += 1 + continue + } + try { + const raw = await fetchBuffer(url) + const webp = await toWebp(raw) + await writeFile(path.join(outDir, `${id}.webp`), webp) + bytes += webp.length + ok += 1 + console.log(`✓ ${id}.webp ${(webp.length / 1024).toFixed(1)}KB`) + } catch (err) { + fail += 1 + console.error(`✗ ${id}: ${err.message}`) + } + } + + for (const cat of data.categories) { + for (const group of cat.groups) { + for (const link of group.links) { + link.icon = `/icons/${link.id}.webp` + } + } + } + await writeFile(linksPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8') + + console.log( + `\nDone: ${ok} ok, ${fail} fail, total ${(bytes / 1024).toFixed(1)}KB → public/icons/`, + ) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..81449b3 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,247 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { CategoryPanel } from './components/CategoryPanel' +import { SideDrawer } from './components/SideDrawer' +import config from './data/links.json' +import type { NavConfig, NavLink } from './types' + +const nav = config as NavConfig + +type Theme = 'light' | 'dark' + +function readTheme(): Theme { + const saved = localStorage.getItem('navmini-theme') + if (saved === 'light' || saved === 'dark') return saved + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' +} + +function IconMenu({ open }: { open: boolean }) { + return ( + + + + + + ) +} + +function IconSun() { + return ( + + + + + ) +} + +function IconMoon() { + return ( + + + + ) +} + +function IconUp() { + return ( + + + + ) +} + +export default function App() { + const [theme, setTheme] = useState('light') + const [activeId, setActiveId] = useState(nav.categories[0]?.id ?? '') + const [drawerOpen, setDrawerOpen] = useState(false) + const [toast, setToast] = useState('') + const [showTop, setShowTop] = useState(false) + const [pillKey, setPillKey] = useState(0) + const jumpingRef = useRef(false) + const themeBtnRef = useRef(null) + + useEffect(() => { + setTheme(readTheme()) + }, []) + + useEffect(() => { + document.documentElement.dataset.theme = theme + localStorage.setItem('navmini-theme', theme) + const meta = document.querySelector('meta[name="theme-color"]') + if (meta) meta.setAttribute('content', theme === 'dark' ? '#0a0f14' : '#e9eef4') + }, [theme]) + + useEffect(() => { + if (!toast) return + const id = window.setTimeout(() => setToast(''), 1600) + return () => window.clearTimeout(id) + }, [toast]) + + useEffect(() => { + const sections = nav.categories + .map((c) => document.getElementById(`cat-${c.id}`)) + .filter((el): el is HTMLElement => !!el) + + const onScroll = () => { + setShowTop(window.scrollY > 480) + if (jumpingRef.current || sections.length === 0) return + + const marker = 88 + let current = sections[0].id.replace(/^cat-/, '') + for (const el of sections) { + const top = el.getBoundingClientRect().top + if (top <= marker) current = el.id.replace(/^cat-/, '') + else break + } + setActiveId((prev) => { + if (prev === current) return prev + setPillKey((k) => k + 1) + return current + }) + } + + onScroll() + window.addEventListener('scroll', onScroll, { passive: true }) + return () => window.removeEventListener('scroll', onScroll) + }, []) + + const activeCategory = useMemo( + () => nav.categories.find((c) => c.id === activeId) ?? nav.categories[0], + [activeId], + ) + + const onOpen = useCallback((link: NavLink) => { + const action = link.action ?? 'open' + if (action === 'copy') { + navigator.clipboard + ?.writeText(link.href) + .then(() => setToast('已复制')) + .catch(() => setToast('复制失败')) + return + } + window.open(link.href, '_blank', 'noopener,noreferrer') + }, []) + + const onSelectCategory = useCallback((id: string) => { + setActiveId(id) + setPillKey((k) => k + 1) + setDrawerOpen(false) + jumpingRef.current = true + const el = document.getElementById(`cat-${id}`) + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + window.setTimeout(() => { + jumpingRef.current = false + }, 700) + }, []) + + function toggleTheme() { + themeBtnRef.current?.classList.remove('spin') + void themeBtnRef.current?.offsetWidth + themeBtnRef.current?.classList.add('spin') + setTheme((t) => (t === 'dark' ? 'light' : 'dark')) + } + + return ( +
+
+ + + +
+ +
+
+ + +
+ +
+

{nav.brand}

+

+ {activeCategory.title} +

+
+
+ + +
+
+ + setDrawerOpen(false)} + onSelect={onSelectCategory} + /> + +
+ {nav.categories.map((cat) => ( + + ))} +
{nav.footer}
+
+ + + +
+ {toast} +
+
+ ) +} diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/components/CategoryPanel.tsx b/src/components/CategoryPanel.tsx new file mode 100644 index 0000000..6e7bc55 --- /dev/null +++ b/src/components/CategoryPanel.tsx @@ -0,0 +1,48 @@ +import { useReveal } from '../hooks/useReveal' +import { LinkCard } from './LinkCard' +import type { NavCategory, NavLink } from '../types' + +interface CategoryPanelProps { + category: NavCategory + onOpen: (link: NavLink) => void +} + +export function CategoryPanel({ category, onOpen }: CategoryPanelProps) { + const ref = useReveal() + let flatIndex = 0 + const count = category.groups.reduce((s, g) => s + g.links.length, 0) + + if (count === 0) { + return ( +
+
+

{category.title}

+
+

暂无入口

+
+ ) + } + + return ( +
+
+

{category.title}

+ {count} +
+ + {category.groups.map((group) => ( +
+

{group.title}

+
+ {group.links.map((link) => { + const i = flatIndex++ + return ( + + ) + })} +
+
+ ))} +
+ ) +} diff --git a/src/components/LinkCard.tsx b/src/components/LinkCard.tsx new file mode 100644 index 0000000..830af31 --- /dev/null +++ b/src/components/LinkCard.tsx @@ -0,0 +1,57 @@ +import { useRef, type MouseEvent } from 'react' +import type { NavLink } from '../types' + +interface LinkCardProps { + link: NavLink + index: number + onOpen: (link: NavLink) => void +} + +export function LinkCard({ link, index, onOpen }: LinkCardProps) { + const btnRef = useRef(null) + + function handleClick(e: MouseEvent) { + const btn = btnRef.current + if (btn && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + const rect = btn.getBoundingClientRect() + const x = e.clientX - rect.left + const y = e.clientY - rect.top + const ripple = document.createElement('span') + ripple.className = 'ripple' + ripple.style.left = `${x}px` + ripple.style.top = `${y}px` + btn.appendChild(ripple) + ripple.addEventListener('animationend', () => ripple.remove()) + } + onOpen(link) + } + + return ( + + ) +} diff --git a/src/components/SideDrawer.tsx b/src/components/SideDrawer.tsx new file mode 100644 index 0000000..a95c9aa --- /dev/null +++ b/src/components/SideDrawer.tsx @@ -0,0 +1,101 @@ +import { useEffect } from 'react' +import type { NavCategory } from '../types' + +interface SideDrawerProps { + open: boolean + brand: string + tagline: string + categories: NavCategory[] + activeId: string + onClose: () => void + onSelect: (id: string) => void +} + +export function SideDrawer({ + open, + brand, + tagline, + categories, + activeId, + onClose, + onSelect, +}: SideDrawerProps) { + useEffect(() => { + if (!open) return + const prev = document.body.style.overflow + document.body.style.overflow = 'hidden' + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + window.addEventListener('keydown', onKey) + return () => { + document.body.style.overflow = prev + window.removeEventListener('keydown', onKey) + } + }, [open, onClose]) + + return ( +
+ +
+ + + + ) +} diff --git a/src/data/links.json b/src/data/links.json new file mode 100644 index 0000000..8075e86 --- /dev/null +++ b/src/data/links.json @@ -0,0 +1,585 @@ +{ + "brand": "游民导航", + "tagline": "数字游民工具与出海资源合集", + "footer": "2026 游民导航", + "categories": [ + { + "id": "nomad", + "title": "数字游民", + "emoji": "✦", + "groups": [ + { + "id": "community", + "title": "游民社区", + "links": [ + { + "id": "nomadro", + "title": "nomadro", + "subtitle": "出海导航", + "href": "https://nomadro.com", + "icon": "/icons/nomadro.webp" + }, + { + "id": "nomadlist", + "title": "nomadlist", + "subtitle": "海外最大的数字游民社区", + "href": "https://nomads.com?ref=https://nomadro.com", + "icon": "/icons/nomadlist.webp" + }, + { + "id": "nomadcna", + "title": "nomadcna", + "subtitle": "国内数字游民社区", + "href": "https://meetup.hackrobot.cn", + "icon": "/icons/nomadcna.webp" + } + ] + }, + { + "id": "toolbox", + "title": "工具箱", + "links": [ + { + "id": "chatgpt", + "title": "ChatGPT", + "subtitle": "OpenAI 开发的人工智能聊天机器人", + "href": "https://chat.openai.com/", + "icon": "/icons/chatgpt.webp" + }, + { + "id": "grok", + "title": "Grok", + "subtitle": "xAI 免费 AI 助手,追求真实与客观", + "href": "https://grok.com/", + "icon": "/icons/grok.webp" + } + ] + }, + { + "id": "us-phone", + "title": "美国手机号", + "links": [ + { + "id": "1psim", + "title": "1psim", + "subtitle": "低成本 2.5$/m,支持 eSIM 和实体卡(邮寄中国)", + "href": "https://1psim.com/7GARRL", + "icon": "/icons/1psim.webp" + }, + { + "id": "yesim", + "title": "yesim", + "subtitle": "美国手机号,3$/m", + "href": "https://yesim.app/?partner_id=1948", + "icon": "/icons/yesim.webp" + }, + { + "id": "tello", + "title": "tello", + "subtitle": "最低 5$/m,支持 eSIM", + "href": "https://tello.com/", + "icon": "/icons/tello.webp" + }, + { + "id": "9esim", + "title": "9esim", + "subtitle": "让任何手机支持 eSIM,可写数据的 SIM 实体卡", + "href": "https://www.9esim.com/?coupon=XIAOSHUANGERIC2", + "icon": "/icons/9esim.webp" + } + ] + }, + { + "id": "finance", + "title": "金融收款", + "links": [ + { + "id": "wise", + "title": "wise", + "subtitle": "线上开通境外银行账户(身份证即可)", + "href": "https://wise.com/invite/amc/xiaoshuangh", + "icon": "/icons/wise.webp" + }, + { + "id": "okx", + "title": "OKX 交易所", + "subtitle": "加密货币交易所", + "href": "https://okx.com/join/84999861", + "icon": "/icons/okx.webp" + }, + { + "id": "safepal", + "title": "safepal (U卡)", + "subtitle": "邀请码 104809,出海神卡,加密货币与法币转换", + "href": "https://www.safepal.com/zh-cn/", + "icon": "/icons/safepal.webp" + }, + { + "id": "paypal", + "title": "paypal", + "subtitle": "出海收款必备,TikTok 提现、佣金提现", + "href": "https://py.pl/1tyAwY", + "icon": "/icons/paypal.webp" + }, + { + "id": "bmc", + "title": "buy me coffee", + "subtitle": "赞助收款平台", + "href": "https://www.buymeacoffee.com/invite/xiaoshuangi", + "icon": "/icons/bmc.webp" + } + ] + } + ] + }, + { + "id": "studio", + "title": "工作室", + "emoji": "☕", + "groups": [ + { + "id": "proxy", + "title": "科学上网", + "links": [ + { + "id": "jms", + "title": "just my socks", + "subtitle": "老牌网络代理服务", + "href": "https://justmysocks.net/members/aff.php?aff=12475", + "icon": "/icons/jms.webp" + }, + { + "id": "ykk", + "title": "Ykk Cloud", + "subtitle": "性价比高的网络加速服务", + "href": "https://panel.ykkk.best/#/register?code=xYQebIgT", + "icon": "/icons/ykk.webp" + } + ] + }, + { + "id": "vps", + "title": "VPS 服务器", + "links": [ + { + "id": "vultr", + "title": "vultr", + "subtitle": "可按分钟计费的服务器(可支付宝)", + "href": "https://www.vultr.com/?ref=8985003", + "icon": "/icons/vultr.webp" + }, + { + "id": "bwh", + "title": "bandwagonhost", + "subtitle": "针对中国线路优化的高质量 VPS", + "href": "https://bandwagonhost.com/aff.php?aff=25463", + "icon": "/icons/bwh.webp" + }, + { + "id": "racknerd", + "title": "RackNerd", + "subtitle": "最便宜的美国 VPS 服务器", + "href": "https://my.racknerd.com/aff.php?aff=15630", + "icon": "/icons/racknerd.webp" + }, + { + "id": "qcloud", + "title": "腾讯云", + "subtitle": "腾讯服务器,国内速度快", + "href": "https://curl.qcloud.com/BZZ2MBTN", + "icon": "/icons/qcloud.webp" + }, + { + "id": "aliyun", + "title": "阿里云", + "subtitle": "阿里巴巴旗下 VPS 服务器", + "href": "https://www.aliyun.com/minisite/goods?userCode=dii0ofzg", + "icon": "/icons/aliyun.webp" + }, + { + "id": "colocrossing", + "title": "colocrossing", + "subtitle": "美国 VPS 服务商", + "href": "https://cloud.colocrossing.com/aff.php?aff=1846", + "icon": "/icons/colocrossing.webp" + } + ] + }, + { + "id": "resi-ip", + "title": "静态住宅 IP", + "links": [ + { + "id": "iproyal", + "title": "iproyal", + "subtitle": "静态住宅 IP,出海必备", + "href": "https://iproyal.cn/?r=nomadro", + "icon": "/icons/iproyal.webp" + }, + { + "id": "proxy-cheap", + "title": "proxy-cheap", + "subtitle": "便宜的住宅 IP", + "href": "https://app.proxy-cheap.com/r/bmcMoY", + "icon": "/icons/proxy-cheap.webp" + }, + { + "id": "smartproxy", + "title": "smartproxy", + "subtitle": "静态住宅 IP 服务商", + "href": "https://www.smartproxy.org/register/?invitation_code=7RXPLM", + "icon": "/icons/smartproxy.webp" + } + ] + }, + { + "id": "resi-vps", + "title": "住宅 VPS 服务器", + "links": [ + { + "id": "lisa", + "title": "lisa", + "subtitle": "住宅 IP 服务器,TikTok 必备", + "href": "https://lisahost.com/aff.php?aff=7566", + "icon": "/icons/lisa.webp" + }, + { + "id": "voyracloud", + "title": "voyracloud", + "subtitle": "静态住宅 IP 服务器", + "href": "https://www.voyracloud.com/?ref_code=2LJPRTE5", + "icon": "/icons/voyracloud.webp" + } + ] + }, + { + "id": "hardware", + "title": "硬件", + "links": [ + { + "id": "rpi", + "title": "树莓派", + "subtitle": "无与伦比的 ARM 开发板", + "href": "https://uland.taobao.com/coupon/edetail?e=5hkWkiThOmClhHvvyUNXZfh8CuWt5YH5OVuOuRD5gLJMmdsrkidbOWBzzpT26idJPSbzO0ArF68ertbZl9wqRUbeJevxT02sHgNIxyFsmaMOsi2OxhyQb%2BhYUTTNN7GRRSHvQe2jOLZ9pbNCYX0I%2BPP%2BWUTgK%2F%2B0I%2BtaUgbudUxA%2B536asYsLWVfKa%2BhVnND4NyB1fwjO3bJLf667Yy8Q5jB6TX2HR3Qrq8LCw4Dujjqyaqc1rGLUwdQHHM8iY%2BUDMxLRSiIntldeBz90au3REAmOgZ0RtdnfeSBK3KXa0%2FA9snq37kKL6zL4bZ3EAS%2F2oYsLHTsIOxroXBFP6oz%2BA%3D%3D&traceId=213e032e17650447230113327e0c4f&union_lens=lensId%3APUB%401765044691%400b521400_0da4_19af4dca231_da77%400290EZjuZ2xbgOlH3NTYAyI%40eyJmbG9vcklkIjoyMDg3MTcxMDEwLCJzcG1CIjoiiX3BvcnRhbF92Ml90b29sX3NlbGVjdGlvbmxpc3RfZGV0YWlsX2luZGV4X2h0bSIsInNyY0Zsb29ySWQiiOiiI4MDY2NCJ9", + "icon": "/icons/rpi.webp" + }, + { + "id": "mini-pc", + "title": "迷你主机", + "subtitle": "具有性价比的 X86 小主机", + "href": "https://s.click.taobao.com/t?e=m%3D2%26s%3DCb1Quo%2F0Wb5w4vFB6t2Z2ueEDrYVVa64YUrQeSeIhnK53hKxp7mNFuhCZFSw9yq2woqx0EHqItb0JlhLk0Jl4eVWmNyHytKdTKJ80yOOA0Mtd0e5Ku7CIDk5ffgV5fRI1qrpxiwMoCNxc1AtbZGVS936JshS%2F%2FIdwgQgG5hIzyLNEPXytV9ALtCLThlbPuuZLb93Df8fOziGg7JVKZos%2BjLJJgRsfWlWtajntMnDLEHhuApO%2F8klJ62RdK%2B6%2FP7Z6k6s3rDgsC6jO9AJYjY8CXJ%2BwEVkOqHFPSJJXArlDuzso5nyPLY4NPiLqets6lIJ", + "icon": "/icons/mini-pc.webp" + }, + { + "id": "gpu", + "title": "GPU 显卡", + "subtitle": "AI 时代的算力基础设施", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATUJK1olXwEDUFZVCkkRC18IGloWWwALV19aAUonRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoUBWkBGFoSVAcdDRsBVXsQB29pbiZRKGMHKlclCEgVWghcGV91UQoyVW5dCUoWAmYPHl4RbTYCU24fZp-esLulkIK0yd6n5V1eOEonA2kPH1oQWQUFV19VCnsQA2Y4El0dWQ4LU1hBC00SCmcJK2slXjYFVFdJDjlWUXsOaWslXTYBZF5cCEoQAGoLH1kRQQYHV1lbAFcXBWgMGl4RXgUKUllYOEkWAmsBK2vL05J3IB9Uajl8UStqSz4dFQ9jitDJGTlnA2sPH0olImF5ECFaSU0VVAdNQDxuW09KFiw8SyljbW1jcih3JWNxNlwaS0hqWBhoX2sQbQEEVW4", + "icon": "/icons/gpu.webp" + } + ] + }, + { + "id": "software", + "title": "软件", + "links": [ + { + "id": "3x-ui", + "title": "3X-UI", + "subtitle": "科学上网仪表盘", + "href": "https://github.com/MHSanaei/3x-ui", + "icon": "/icons/3x-ui.webp" + }, + { + "id": "mihomo", + "title": "虚空终端 mihomo", + "subtitle": "代理软件以及配置 wiki", + "href": "https://wiki.metacubex.one/config/", + "icon": "/icons/mihomo.webp" + }, + { + "id": "frp", + "title": "frp", + "subtitle": "高性能内网穿透", + "href": "https://gofrp.org/zh-cn/docs/", + "icon": "/icons/frp.webp" + }, + { + "id": "wireguard", + "title": "wireguard", + "subtitle": "P2P 打洞组网", + "href": "https://github.com/WireGuard/wireguard-go", + "icon": "/icons/wireguard.webp" + }, + { + "id": "lineageos", + "title": "lineageos (pi5)", + "subtitle": "树莓派的安卓 LineageOS", + "href": "https://konstakang.com/devices/rpi5/", + "icon": "/icons/lineageos.webp" + } + ] + }, + { + "id": "gear", + "title": "数码装备", + "links": [ + { + "id": "ai-glasses", + "title": "AI 眼镜", + "subtitle": "第一视角拍摄", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATkJK1olXw8AXVxfCU8UA18IGloVWwcKXFlZDUgnRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoXBW4AE1wRWAUdDRsBVXtwXw1pZzNKAmZgFFwlYUh8UzVBZyZlUQoyVW5dCUoWAmYPHl4RbTYCU24fZpO9hbeBtYKjxt-jwInikZ-fjV8JK1sTWgIDUVtVAEkVA2w4HFscbQ8EXFpVAUwRH2wOHlIdXDYyZF1tD0seF2l6WgkBW3QyZF5tC3sXAm8AGFsVXAEBXEJdDUMVB24UG10SWQcHUVZbAE8fCl8KGloRVDYyitPtY0JrQAYIYj0dXGZKMVc5T5Was35jYlMVXQUTZAQOch9SZTMNbC9gIGdQJi5VCzkRSCpfWDUXVAJlDQoCbw1gcG9qRwYdHnYyUW5aDEgn", + "icon": "/icons/ai-glasses.webp" + }, + { + "id": "action-cam", + "title": "运动相机", + "subtitle": "骑行记录仪", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eCE0UB28ME1slGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWXQABUF5ZAEsIWipURmt3CFhXIQkpbChuRy8JGDpRJUJQDwkbBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVpaDEsUM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmsNHl8UXQQeVF1ZDE0UH28OHF8UWAIDUVddCkgnAW4JH1IlbdiMwDxcaAJfBTlNTRpCBHpHLhyDht8GcR8BHFIUTDZ6NSMpYRJweWtBfAUXC2JyXBkpfTNRWi1mGV4UAlgENFpeDA13cRRcTAJLbQMyU1peOA", + "icon": "/icons/action-cam.webp" + }, + { + "id": "drone", + "title": "无人机", + "subtitle": "天空视角", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eAUIeB2YMH14lGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWVA8LUFdZDE4IWipURmtCHHVEABsFVChPYRJAbVhlGRhbFRgLBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVdbC04VM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmgLHlgRXgIeVFpfDU0UH28OHF8UWAIDUlteAUwnAW4JH1IlbdiMwCQvcipMBxJhfV9HGgF3FVaDht8GfxgBGVkRTDZUCjsVbzBTYRJbeRkWGG9WAx02D0JcUy5mGRNRFEJiUwgldStzfx1wTj1GbQMyU1peOA", + "icon": "/icons/drone.webp" + } + ] + } + ] + }, + { + "id": "overseas", + "title": "出海掘金", + "emoji": "◎", + "groups": [ + { + "id": "tiktok", + "title": "TikTok", + "links": [ + { + "id": "tiktok", + "title": "TikTok", + "subtitle": "全球直播平台", + "href": "https://www.tiktok.com/", + "icon": "/icons/tiktok.webp" + }, + { + "id": "cloud-phone", + "title": "云手机", + "subtitle": "用开源硬件打造云手机", + "href": "https://www.hackrobot.cn/", + "icon": "/icons/cloud-phone.webp" + }, + { + "id": "vcam", + "title": "vcam 虚拟摄像头", + "subtitle": "Xposed 模块,替换摄像头视频流", + "href": "https://pan.baidu.com/s/1NW4GMGA3cxfUQNWZPD9vOg?pwd=live", + "icon": "/icons/vcam.webp" + } + ] + }, + { + "id": "youtube", + "title": "YouTube", + "links": [ + { + "id": "youtube", + "title": "YouTube", + "subtitle": "全球最大的视频网站", + "href": "https://www.youtube.com/", + "icon": "/icons/youtube.webp" + }, + { + "id": "smmfollows", + "title": "smmfollows", + "subtitle": "刷粉刷量平台", + "href": "https://smmfollows.com/ref/dfugq", + "icon": "/icons/smmfollows.webp" + }, + { + "id": "yt-auto-dismiss", + "title": "YouTube Studio Auto Dismiss", + "subtitle": "油猴脚本,无人直播辅助", + "href": "https://greasyfork.org/zh-CN/scripts/557378-youtube-studio-auto-dismiss", + "icon": "/icons/yt-auto-dismiss.webp" + } + ] + }, + { + "id": "web", + "title": "Web 网站", + "links": [ + { + "id": "namesilo", + "title": "namesilo", + "subtitle": "注册域名", + "href": "https://www.namesilo.com/?rid=3bd4047mc", + "icon": "/icons/namesilo.webp" + }, + { + "id": "aapanel", + "title": "aapanel", + "subtitle": "宝塔面板", + "href": "https://www.aapanel.com?referral_code=wcn04vez", + "icon": "/icons/aapanel.webp" + }, + { + "id": "cloudflare", + "title": "Cloudflare", + "subtitle": "互联网基础节点", + "href": "https://www.cloudflare.com/", + "icon": "/icons/cloudflare.webp" + }, + { + "id": "ga", + "title": "Google Analytics", + "subtitle": "网站流量统计服务", + "href": "https://analytics.google.com/", + "icon": "/icons/ga.webp" + }, + { + "id": "adsense", + "title": "Google AdSense", + "subtitle": "Google 广告服务", + "href": "https://www.google.com/adsense/", + "icon": "/icons/adsense.webp" + } + ] + } + ] + }, + { + "id": "live", + "title": "无人直播", + "emoji": "▷", + "groups": [ + { + "id": "live-soft", + "title": "软件", + "links": [ + { + "id": "obs", + "title": "OBS", + "subtitle": "免费开源:录屏 / 推流直播", + "href": "https://pan.quark.cn/s/03e89f1bbc9b", + "icon": "/icons/obs.webp" + }, + { + "id": "ffmpeg", + "title": "ffmpeg", + "subtitle": "命令行视频处理 / 推流,Python 自动化", + "href": "https://www.ffmpeg.org/download.html", + "icon": "/icons/ffmpeg.webp" + }, + { + "id": "srs", + "title": "SRS", + "subtitle": "开源直播服务器,拉流推流中转", + "href": "https://ossrs.io/", + "icon": "/icons/srs.webp" + } + ] + } + ] + }, + { + "id": "pay", + "title": "支付网关", + "emoji": "◇", + "groups": [ + { + "id": "merchant", + "title": "小微商户", + "links": [ + { + "id": "nowpayments", + "title": "NOWPayments", + "subtitle": "加密货币收款", + "href": "https://account.nowpayments.io/create-account?link_id=3680795921", + "icon": "/icons/nowpayments.webp" + }, + { + "id": "creem", + "title": "Creem", + "subtitle": "软件出海收款,税务合规", + "href": "https://www.creem.io/", + "icon": "/icons/creem.webp" + }, + { + "id": "stripe", + "title": "Stripe", + "subtitle": "全球在线支付", + "href": "https://stripe.com/", + "icon": "/icons/stripe.webp" + }, + { + "id": "zpay", + "title": "z-pay", + "subtitle": "支持独角数卡 · 易支付", + "href": "https://z-pay.cn/?uid=22119", + "icon": "/icons/zpay.webp" + }, + { + "id": "xorpay", + "title": "xorpay", + "subtitle": "支持收银台支付", + "href": "https://xorpay.com?r=nomadro", + "icon": "/icons/xorpay.webp" + } + ] + } + ] + }, + { + "id": "books", + "title": "书籍", + "emoji": "◈", + "groups": [ + { + "id": "handbook", + "title": "手册", + "links": [ + { + "id": "nomad-book", + "title": "数字游民手册", + "subtitle": "复制链接后在小红书打开", + "href": "https://www.xiaohongshu.com/s/poster?bgimg=https%3A%2F%2Fsns-redskillhub-s1.xhscdn.com%2Fred_app_image%2F1040g4m8325ke1q36l0404a4druqrlvd5valvb80%3Fsign%3Dbc0c41c2e219f6ca904f52176bd17f54%26t%3D6c9a7a85&xhsshare=&appuid=5acc75ba4eacab15c751fda5&apptime=1790527237&deeplink=xhsdiscover%3A%2F%2FminiTool%2F6ab8984da1e3af0015584e92%3Fsource%3Dh5%26page_key%3D28%26xhsMpScreenMode%3Dfull%26share_id%3D5e90e5979b054c8baf8b3db4c8993d79%26share_channel%3Dcopy_link", + "icon": "/icons/nomad-book.webp", + "action": "copy" + } + ] + } + ] + }, + { + "id": "forum", + "title": "论坛社区", + "emoji": "◉", + "groups": [ + { + "id": "blog", + "title": "博客", + "links": [ + { + "id": "yidu", + "title": "异度世界", + "subtitle": "独立博客,Hugo 搭建", + "href": "https://blog.hackrobot.cn", + "icon": "/icons/yidu.webp" + }, + { + "id": "dsx", + "title": "大师兄", + "subtitle": "AI 实践", + "href": "https://dsx2016.com", + "icon": "/icons/dsx.webp" + } + ] + } + ] + } + ] +} diff --git a/src/hooks/usePrefersReducedMotion.ts b/src/hooks/usePrefersReducedMotion.ts new file mode 100644 index 0000000..8591ae6 --- /dev/null +++ b/src/hooks/usePrefersReducedMotion.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react' + +export function usePrefersReducedMotion() { + const [reduced, setReduced] = useState(false) + + useEffect(() => { + const mq = window.matchMedia('(prefers-reduced-motion: reduce)') + const update = () => setReduced(mq.matches) + update() + mq.addEventListener('change', update) + return () => mq.removeEventListener('change', update) + }, []) + + return reduced +} diff --git a/src/hooks/useReveal.ts b/src/hooks/useReveal.ts new file mode 100644 index 0000000..a003190 --- /dev/null +++ b/src/hooks/useReveal.ts @@ -0,0 +1,37 @@ +import { useEffect, useRef } from 'react' +import { usePrefersReducedMotion } from './usePrefersReducedMotion' + +/** Reveal children with `.reveal` when the root enters the viewport. */ +export function useReveal() { + const ref = useRef(null) + const reduced = usePrefersReducedMotion() + + useEffect(() => { + const root = ref.current + if (!root) return + + if (reduced) { + root.classList.add('is-visible') + root.querySelectorAll('.reveal').forEach((el) => el.classList.add('is-visible')) + return + } + + const targets = [root, ...root.querySelectorAll('.reveal')] + + const io = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (!entry.isIntersecting) continue + entry.target.classList.add('is-visible') + io.unobserve(entry.target) + } + }, + { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }, + ) + + for (const el of targets) io.observe(el) + return () => io.disconnect() + }, [reduced]) + + return ref +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..33f95d7 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.tsx' +import './styles/global.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..91c2bf8 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,951 @@ +:root, +[data-theme='light'] { + --bg: #e9eef4; + --bg-elevated: #f7fafc; + --ink: #15202b; + --ink-muted: #5b6b7c; + --ink-faint: #8a98a8; + --line: rgba(21, 32, 43, 0.08); + --surface: rgba(255, 255, 255, 0.78); + --surface-solid: #ffffff; + --surface-hover: #ffffff; + --accent: #0d8f7b; + --accent-deep: #0a6e5f; + --accent-soft: rgba(13, 143, 123, 0.12); + --glow-a: rgba(13, 143, 123, 0.2); + --glow-b: rgba(56, 120, 180, 0.14); + --shadow: 0 8px 24px rgba(21, 32, 43, 0.06); + --shadow-card: 0 1px 2px rgba(21, 32, 43, 0.04), 0 6px 16px rgba(21, 32, 43, 0.04); + --toast-bg: #15202b; + --toast-ink: #f4f7fa; + --chip: rgba(21, 32, 43, 0.045); + --chip-active: #15202b; + --chip-active-ink: #f7fafc; + --topbar-bg: rgba(233, 238, 244, 0.78); + --rail: #0d8f7b; + --font-display: 'Sora', system-ui, sans-serif; + --font-body: 'Manrope', 'PingFang SC', 'Noto Sans SC', system-ui, sans-serif; + --radius: 14px; + --max-w: 560px; +} + +[data-theme='dark'] { + --bg: #0a0f14; + --bg-elevated: #121820; + --ink: #e8eef4; + --ink-muted: #8b99a8; + --ink-faint: #5d6c7c; + --line: rgba(232, 238, 244, 0.1); + --surface: rgba(18, 24, 32, 0.88); + --surface-solid: #121820; + --surface-hover: #18212c; + --accent: #3dceb8; + --accent-deep: #2aa892; + --accent-soft: rgba(61, 206, 184, 0.14); + --glow-a: rgba(61, 206, 184, 0.14); + --glow-b: rgba(70, 130, 200, 0.12); + --shadow: 0 10px 28px rgba(0, 0, 0, 0.35); + --shadow-card: 0 1px 0 rgba(255, 255, 255, 0.03), 0 8px 20px rgba(0, 0, 0, 0.28); + --toast-bg: #e8eef4; + --toast-ink: #0a0f14; + --chip: rgba(232, 238, 244, 0.06); + --chip-active: #e8eef4; + --chip-active-ink: #0a0f14; + --topbar-bg: rgba(10, 15, 20, 0.82); + --rail: #3dceb8; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#root { + margin: 0; + min-height: 100%; +} + +body { + font-family: var(--font-body); + color: var(--ink); + background: var(--bg); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +button, +input { + font: inherit; + color: inherit; +} + +button { + border: none; + background: none; + cursor: pointer; + padding: 0; +} + +.app-shell { + position: relative; + min-height: 100dvh; + isolation: isolate; +} + +.bg-glow { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + overflow: hidden; + background: + radial-gradient(680px 380px at 8% -12%, var(--glow-a), transparent 58%), + radial-gradient(560px 320px at 100% 4%, var(--glow-b), transparent 55%), + radial-gradient(420px 280px at 50% 100%, rgba(13, 143, 123, 0.06), transparent 60%), + var(--bg); +} + +.bg-glow::after { + content: ''; + position: absolute; + inset: 0; + opacity: 0.35; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E"); + background-size: 180px 180px; + mix-blend-mode: soft-light; + animation: grain 8s steps(6) infinite; +} + +.orb { + position: absolute; + border-radius: 50%; + filter: blur(2px); + opacity: 0.55; + will-change: transform; +} + +.orb-a { + width: 220px; + height: 220px; + left: -70px; + top: 12%; + background: radial-gradient(circle, var(--glow-a), transparent 70%); + animation: drift-a 16s ease-in-out infinite; +} + +.orb-b { + width: 260px; + height: 260px; + right: -90px; + top: 38%; + background: radial-gradient(circle, var(--glow-b), transparent 70%); + animation: drift-b 19s ease-in-out infinite; +} + +.orb-c { + width: 180px; + height: 180px; + left: 35%; + bottom: 8%; + background: radial-gradient(circle, rgba(13, 143, 123, 0.18), transparent 70%); + animation: drift-c 14s ease-in-out infinite; +} + +.topbar { + padding: 10px 14px; +} + +.brand-row { + display: flex; + align-items: center; + gap: 10px; +} + +.icon-btn { + width: 36px; + height: 36px; + border-radius: 11px; + background: var(--surface); + border: 1px solid var(--line); + box-shadow: var(--shadow-card); + display: grid; + place-items: center; + flex-shrink: 0; + color: var(--ink); + transition: + transform 0.15s ease, + background 0.2s ease, + border-color 0.2s ease; +} + +.icon-btn:hover { + border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); +} + +.icon-btn:active { + transform: scale(0.94); +} + +.icon-btn svg { + width: 18px; + height: 18px; + display: block; +} + +.brand-mark { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1; +} + +.brand-mark img { + width: 28px; + height: 28px; + border-radius: 9px; + object-fit: cover; + flex-shrink: 0; + box-shadow: var(--shadow-card); + border: 1px solid var(--line); +} + +.brand-text { + min-width: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 2px; +} + +.brand-name { + margin: 0; + font-family: var(--font-display); + font-size: 1.02rem; + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.2; +} + +.brand-current { + margin: 0; + max-width: 100%; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: -0.01em; + line-height: 1.25; + color: var(--accent-deep); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + animation: pill-in 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.brand-logo { + animation: logo-float 5.5s ease-in-out infinite; +} + +.menu-icon .menu-line { + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.2s ease; + transform-origin: center; +} + +.menu-trigger.open .menu-line.top { + transform: translateY(5px) rotate(45deg); +} + +.menu-trigger.open .menu-line.mid { + opacity: 0; + transform: scaleX(0.2); +} + +.menu-trigger.open .menu-line.bot { + transform: translateY(-5px) translateX(2px) rotate(-45deg); + /* bot path is shorter; stretch feel via rotate only */ +} + +.theme-trigger.spin svg { + animation: theme-spin 0.45s cubic-bezier(0.22, 1, 0.36, 1); +} + +html { + transition: background-color 0.35s ease, color 0.35s ease; +} + +[data-theme='dark'] .brand-current { + color: var(--accent); +} + +.sticky-stack { + position: sticky; + top: 0; + z-index: 30; + width: min(100%, var(--max-w)); + margin: 0 auto; + background: var(--topbar-bg); + backdrop-filter: blur(18px) saturate(1.2); + -webkit-backdrop-filter: blur(18px) saturate(1.2); + border-bottom: 1px solid var(--line); +} + +.drawer-root { + position: fixed; + inset: 0; + z-index: 60; + pointer-events: none; +} + +.drawer-root.open { + pointer-events: auto; +} + +.drawer-mask { + position: absolute; + inset: 0; + border: none; + padding: 0; + margin: 0; + background: rgba(8, 12, 18, 0.46); + opacity: 0; + transition: opacity 0.24s ease; + cursor: pointer; + backdrop-filter: blur(2px); +} + +.drawer-root.open .drawer-mask { + opacity: 1; +} + +.drawer-panel { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: min(80vw, 310px); + background: + linear-gradient(180deg, color-mix(in srgb, var(--accent) 8%, var(--bg-elevated)), var(--bg-elevated) 28%), + var(--bg-elevated); + border-right: 1px solid var(--line); + box-shadow: var(--shadow); + transform: translateX(-104%); + transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1); + display: flex; + flex-direction: column; + padding: calc(12px + env(safe-area-inset-top, 0px)) 0 + calc(18px + env(safe-area-inset-bottom, 0px)); +} + +.drawer-root.open .drawer-panel { + transform: translateX(0); +} + +.drawer-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 6px 16px 14px; + border-bottom: 1px solid var(--line); +} + +.drawer-brand-wrap { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.drawer-brand-wrap img { + width: 28px; + height: 28px; + border-radius: 8px; + object-fit: cover; + border: 1px solid var(--line); +} + +.drawer-brand { + font-family: var(--font-display); + font-weight: 700; + font-size: 1.05rem; + letter-spacing: -0.03em; +} + +.drawer-sub { + display: block; + margin-top: 2px; + font-size: 0.68rem; + color: var(--ink-muted); +} + +.drawer-nav { + display: flex; + flex-direction: column; + gap: 4px; + padding: 14px 10px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +.drawer-item { + position: relative; + display: flex; + align-items: center; + gap: 10px; + width: 100%; + text-align: left; + padding: 12px 12px 12px 14px; + border-radius: 14px; + color: var(--ink-muted); + opacity: 0; + transform: translateX(-12px); + transition: + background 0.18s ease, + color 0.18s ease, + transform 0.12s ease; +} + +.drawer-root.open .drawer-item { + animation: drawer-item-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; + animation-delay: calc(0.05s + var(--i, 0) * 0.045s); +} + +.drawer-item:active { + transform: scale(0.985); +} + +.drawer-item.active { + background: var(--accent-soft); + color: var(--accent); +} + +.drawer-item.active::before { + content: ''; + position: absolute; + left: 4px; + top: 10px; + bottom: 10px; + width: 3px; + border-radius: 99px; + background: var(--rail); +} + +.drawer-emoji { + width: 1.25rem; + text-align: center; + font-size: 0.9rem; + flex-shrink: 0; +} + +.drawer-item-title { + flex: 1; + min-width: 0; + font-size: 0.94rem; + font-weight: 600; + color: inherit; +} + +.drawer-item.active .drawer-item-title { + color: var(--ink); +} + +.drawer-item-count { + font-size: 0.68rem; + font-weight: 650; + color: var(--ink-faint); + background: var(--chip); + padding: 2px 7px; + border-radius: 999px; +} + +.drawer-item.active .drawer-item-count { + background: color-mix(in srgb, var(--accent) 18%, transparent); + color: var(--accent-deep); +} + +[data-theme='dark'] .drawer-item.active .drawer-item-count { + color: var(--accent); +} + +.page { + position: relative; + z-index: 1; + width: min(100%, var(--max-w)); + margin: 0 auto; + padding: 14px 12px 80px; +} + +.cat-panel + .cat-panel { + margin-top: 26px; +} + +.cat-panel { + scroll-margin-top: 62px; +} + +.cat-heading { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; + padding-left: 10px; + position: relative; +} + +.cat-heading::before { + content: ''; + position: absolute; + left: 0; + top: 0.2em; + bottom: 0.2em; + width: 3px; + border-radius: 99px; + background: linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 20%, transparent)); +} + +.cat-heading h2 { + margin: 0; + font-family: var(--font-display); + font-size: 1.08rem; + font-weight: 700; + letter-spacing: -0.03em; +} + +.cat-count { + font-size: 0.66rem; + background: var(--accent-soft); + color: var(--accent-deep); + padding: 2px 7px; + border-radius: 999px; + font-weight: 700; +} + +[data-theme='dark'] .cat-count { + color: var(--accent); +} + +.group-block + .group-block { + margin-top: 14px; +} + +.group-title { + margin: 0 0 7px; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--ink-faint); +} + +.link-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 7px; +} + +@media (min-width: 560px) { + .link-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +.link-card { + position: relative; + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-width: 0; + text-align: left; + padding: 9px; + border-radius: var(--radius); + background: var(--surface); + border: 1px solid var(--line); + box-shadow: var(--shadow-card); + overflow: hidden; + transition: + transform 0.16s ease, + background 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; +} + +.link-card:hover { + background: var(--surface-hover); + border-color: color-mix(in srgb, var(--accent) 32%, var(--line)); + box-shadow: 0 8px 22px rgba(13, 143, 123, 0.1); +} + +.link-card:hover .link-icon-wrap { + transform: scale(1.06) rotate(-2deg); +} + +.link-card:active { + transform: scale(0.978); +} + +.link-icon-wrap { + width: 34px; + height: 34px; + border-radius: 10px; + background: linear-gradient(160deg, var(--chip), transparent); + border: 1px solid var(--line); + display: grid; + place-items: center; + overflow: hidden; + flex-shrink: 0; + transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1); +} + +.ripple { + position: absolute; + width: 8px; + height: 8px; + margin: -4px 0 0 -4px; + border-radius: 50%; + background: color-mix(in srgb, var(--accent) 35%, transparent); + pointer-events: none; + animation: ripple 0.55s ease-out forwards; +} + +.link-icon { + width: 100%; + height: 100%; + object-fit: cover; +} + +.link-meta { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.link-title { + font-size: 0.82rem; + font-weight: 700; + letter-spacing: -0.01em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.link-desc { + font-size: 0.65rem; + color: var(--ink-muted); + line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.link-arrow { + display: none; +} + +.empty-hint { + margin: 16px 0; + text-align: center; + color: var(--ink-muted); + font-size: 0.84rem; +} + +.footer { + margin-top: 28px; + padding-top: 16px; + border-top: 1px solid var(--line); + text-align: center; + font-size: 0.7rem; + color: var(--ink-faint); + letter-spacing: 0.02em; +} + +.back-top { + position: fixed; + right: max(14px, calc((100vw - var(--max-w)) / 2 + 14px)); + bottom: 20px; + width: 40px; + height: 40px; + border-radius: 14px; + background: var(--chip-active); + color: var(--chip-active-ink); + box-shadow: var(--shadow); + opacity: 0; + pointer-events: none; + transform: translateY(10px) scale(0.96); + transition: + opacity 0.22s ease, + transform 0.22s ease; + z-index: 40; + display: grid; + place-items: center; +} + +.back-top svg { + width: 18px; + height: 18px; +} + +.back-top.show { + opacity: 1; + pointer-events: auto; + transform: translateY(0) scale(1); + animation: back-top-in 0.35s cubic-bezier(0.22, 1, 0.36, 1); +} + +.back-top:hover svg { + animation: nudge-up 0.7s ease-in-out infinite; +} + +.toast { + position: fixed; + left: 50%; + bottom: 24px; + transform: translateX(-50%) translateY(10px); + padding: 9px 16px; + border-radius: 999px; + background: var(--toast-bg); + color: var(--toast-ink); + font-size: 0.8rem; + font-weight: 600; + opacity: 0; + pointer-events: none; + transition: + opacity 0.2s ease, + transform 0.2s ease; + z-index: 50; + box-shadow: var(--shadow); +} + +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); + animation: toast-pop 0.35s cubic-bezier(0.22, 1, 0.36, 1); +} + +/* Scroll reveal */ +.reveal { + opacity: 0; + transform: translateY(14px); + transition: + opacity 0.45s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.45s cubic-bezier(0.22, 1, 0.36, 1); + transition-delay: var(--reveal-delay, 0ms); +} + +.reveal.is-visible { + opacity: 1; + transform: translateY(0); +} + +.cat-heading.reveal.is-visible::before { + animation: rail-grow 0.45s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +@keyframes rise { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes intro-card { + from { + opacity: 0; + transform: translateY(16px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes intro-glow { + 0%, + 100% { + transform: scale(1); + opacity: 0.9; + } + 50% { + transform: scale(1.15); + opacity: 1; + } +} + +@keyframes pill-in { + from { + opacity: 0; + transform: translateY(4px) scale(0.92); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes logo-float { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-2px); + } +} + +@keyframes theme-spin { + from { + transform: rotate(-90deg) scale(0.7); + opacity: 0.4; + } + to { + transform: rotate(0) scale(1); + opacity: 1; + } +} + +@keyframes drawer-item-in { + from { + opacity: 0; + transform: translateX(-14px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes ripple { + to { + transform: scale(28); + opacity: 0; + } +} + +@keyframes rail-grow { + from { + transform: scaleY(0); + transform-origin: top; + } + to { + transform: scaleY(1); + transform-origin: top; + } +} + +@keyframes drift-a { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(24px, -18px) scale(1.08); + } +} + +@keyframes drift-b { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(-20px, 22px) scale(1.05); + } +} + +@keyframes drift-c { + 0%, + 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(16px, -12px); + } +} + +@keyframes back-top-in { + from { + opacity: 0; + transform: translateY(12px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes nudge-up { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-2px); + } +} + +@keyframes toast-pop { + from { + opacity: 0; + transform: translateX(-50%) translateY(12px) scale(0.94); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0) scale(1); + } +} + +@keyframes grain { + 0%, + 100% { + transform: translate(0, 0); + } + 20% { + transform: translate(-1%, 1%); + } + 40% { + transform: translate(1%, -1%); + } + 60% { + transform: translate(-1%, -1%); + } + 80% { + transform: translate(1%, 1%); + } +} + +@media (prefers-reduced-motion: reduce) { + .bg-glow::after, + .orb, + .brand-logo, + .brand-current, + .drawer-root.open .drawer-item, + .theme-trigger.spin svg, + .back-top.show, + .toast.show, + .cat-heading.reveal.is-visible::before { + animation: none !important; + } + + .reveal, + .drawer-item { + opacity: 1 !important; + transform: none !important; + transition: none !important; + } + + .menu-icon .menu-line { + transition: none; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..8db6890 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,30 @@ +export type LinkAction = 'open' | 'copy' + +export interface NavLink { + id: string + title: string + subtitle: string + href: string + icon: string + action?: LinkAction +} + +export interface NavGroup { + id: string + title: string + links: NavLink[] +} + +export interface NavCategory { + id: string + title: string + emoji: string + groups: NavGroup[] +} + +export interface NavConfig { + brand: string + tagline: string + footer: string + categories: NavCategory[] +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..345ba12 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "resolveJsonModule": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..9982072 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/weapp/README.md b/weapp/README.md new file mode 100644 index 0000000..aabbb3f --- /dev/null +++ b/weapp/README.md @@ -0,0 +1,47 @@ +# 游民导航 · 微信小程序 + +原生微信小程序,数据与图标同步自仓库 H5:`src/data/links.json`、`public/icons/`。 + +交互对齐 H5:左侧抽屉跳转分类、整页长列表、浅/深色主题、回到顶部。因小程序无法直接打开任意外链,点击入口会弹出底部面板,**一键复制网址**后到浏览器粘贴打开(微信允许 `wx.setClipboardData`)。 + +结构参考 [nomadmini/miniapp](../../nomadmini/miniapp) 的 DevTools 接入方式(`project.config.json` + 开发者工具打开目录);实现上用原生小程序而非 Taro,更适合静态导航包。 + +## 本地预览 + +1. 同步数据与图标: + +```bash +npm run weapp:sync +``` + +或在 `weapp/` 下: + +```powershell +python scripts/sync_data.py +``` + +2. 打开 **微信开发者工具** → 导入项目 → 目录选本仓库的 **`weapp/`**(不要选上级)。 + +3. AppID:可用测试号,或把 `project.config.json` 里的 `touristappid` 换成你的小程序 AppID。 + +4. 详情 → 本地设置 → **不校验合法域名**(开发期)。 + +## 目录 + +``` +weapp/ +├── app.js / app.json / app.wxss +├── project.config.json +├── data/links.json # sync 生成,勿手改 +├── icons/ # sync 生成 +├── pages/index/ # 唯一页面 +└── scripts/sync_data.py +``` + +## 与 H5 / 小红书差异 + +| | H5 | 微信 weapp | 小红书 xhs | +|--|----|-----------|------------| +| 打开链接 | `window.open` | 复制到剪贴板 | 手动长按复制 | +| 数据 | `src/data/links.json` | sync → `data/` + `icons/` | sync → zip | +| 运行 | Vite | 微信开发者工具 | 创作平台上传 zip | diff --git a/weapp/app.js b/weapp/app.js new file mode 100644 index 0000000..370f7e4 --- /dev/null +++ b/weapp/app.js @@ -0,0 +1,15 @@ +App({ + globalData: { + theme: "light", + }, + onLaunch() { + try { + const saved = wx.getStorageSync("navmini-theme"); + if (saved === "light" || saved === "dark") { + this.globalData.theme = saved; + } + } catch (e) { + /* ignore */ + } + }, +}); diff --git a/weapp/app.json b/weapp/app.json new file mode 100644 index 0000000..30d28b6 --- /dev/null +++ b/weapp/app.json @@ -0,0 +1,13 @@ +{ + "pages": ["pages/index/index"], + "window": { + "navigationBarTitleText": "游民导航", + "navigationBarBackgroundColor": "#e9eef4", + "navigationBarTextStyle": "black", + "backgroundColor": "#e9eef4", + "backgroundTextStyle": "dark" + }, + "style": "v2", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents" +} diff --git a/weapp/app.wxss b/weapp/app.wxss new file mode 100644 index 0000000..1d88009 --- /dev/null +++ b/weapp/app.wxss @@ -0,0 +1,80 @@ +page { + background: #e9eef4; + color: #15202b; + font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Noto Sans SC", + "Helvetica Neue", sans-serif; + font-size: 28rpx; + line-height: 1.45; + box-sizing: border-box; +} + +view, +text, +button, +image, +scroll-view { + box-sizing: border-box; +} + +button { + margin: 0; + padding: 0; + background: transparent; + border: none; + line-height: inherit; + font: inherit; + color: inherit; +} + +button::after { + border: none; +} + +.app-shell { + --bg: #e9eef4; + --bg-elevated: #f7fafc; + --ink: #15202b; + --ink-muted: #5b6b7c; + --ink-faint: #8a98a8; + --line: rgba(21, 32, 43, 0.08); + --surface: rgba(255, 255, 255, 0.92); + --surface-solid: #ffffff; + --accent: #0d8f7b; + --accent-deep: #0a6e5f; + --accent-soft: rgba(13, 143, 123, 0.12); + --shadow: 0 8px 24px rgba(21, 32, 43, 0.06); + --shadow-card: 0 1px 2px rgba(21, 32, 43, 0.04), 0 6px 16px rgba(21, 32, 43, 0.04); + --toast-bg: #15202b; + --toast-ink: #f4f7fa; + --chip: rgba(21, 32, 43, 0.045); + --chip-active: #15202b; + --chip-active-ink: #f7fafc; + --topbar-bg: rgba(233, 238, 244, 0.92); + --rail: #0d8f7b; + min-height: 100vh; + background: var(--bg); + color: var(--ink); +} + +.app-shell.theme-dark { + --bg: #0a0f14; + --bg-elevated: #121820; + --ink: #e8eef4; + --ink-muted: #8b99a8; + --ink-faint: #5d6c7c; + --line: rgba(232, 238, 244, 0.1); + --surface: rgba(18, 24, 32, 0.92); + --surface-solid: #121820; + --accent: #3dceb8; + --accent-deep: #2aa892; + --accent-soft: rgba(61, 206, 184, 0.14); + --shadow: 0 10px 28px rgba(0, 0, 0, 0.35); + --shadow-card: 0 1px 0 rgba(255, 255, 255, 0.03), 0 8px 20px rgba(0, 0, 0, 0.28); + --toast-bg: #e8eef4; + --toast-ink: #0a0f14; + --chip: rgba(232, 238, 244, 0.06); + --chip-active: #e8eef4; + --chip-active-ink: #0a0f14; + --topbar-bg: rgba(10, 15, 20, 0.9); + --rail: #3dceb8; +} diff --git a/weapp/data/links.json b/weapp/data/links.json new file mode 100644 index 0000000..1eb03c6 --- /dev/null +++ b/weapp/data/links.json @@ -0,0 +1,586 @@ +{ + "brand": "游民导航", + "tagline": "数字游民工具与出海资源合集", + "footer": "2026 游民导航", + "categories": [ + { + "id": "nomad", + "title": "数字游民", + "emoji": "✦", + "groups": [ + { + "id": "community", + "title": "游民社区", + "links": [ + { + "id": "nomadro", + "title": "nomadro", + "subtitle": "出海导航", + "href": "https://nomadro.com", + "icon": "/icons/nomadro.webp" + }, + { + "id": "nomadlist", + "title": "nomadlist", + "subtitle": "海外最大的数字游民社区", + "href": "https://nomads.com?ref=https://nomadro.com", + "icon": "/icons/nomadlist.webp" + }, + { + "id": "nomadcna", + "title": "nomadcna", + "subtitle": "国内数字游民社区", + "href": "https://meetup.hackrobot.cn", + "icon": "/icons/nomadcna.webp" + } + ] + }, + { + "id": "toolbox", + "title": "工具箱", + "links": [ + { + "id": "chatgpt", + "title": "ChatGPT", + "subtitle": "OpenAI 开发的人工智能聊天机器人", + "href": "https://chat.openai.com/", + "icon": "/icons/chatgpt.webp" + }, + { + "id": "grok", + "title": "Grok", + "subtitle": "xAI 免费 AI 助手,追求真实与客观", + "href": "https://grok.com/", + "icon": "/icons/grok.webp" + } + ] + }, + { + "id": "us-phone", + "title": "美国手机号", + "links": [ + { + "id": "1psim", + "title": "1psim", + "subtitle": "低成本 2.5$/m,支持 eSIM 和实体卡(邮寄中国)", + "href": "https://1psim.com/7GARRL", + "icon": "/icons/1psim.webp" + }, + { + "id": "yesim", + "title": "yesim", + "subtitle": "美国手机号,3$/m", + "href": "https://yesim.app/?partner_id=1948", + "icon": "/icons/yesim.webp" + }, + { + "id": "tello", + "title": "tello", + "subtitle": "最低 5$/m,支持 eSIM", + "href": "https://tello.com/", + "icon": "/icons/tello.webp" + }, + { + "id": "9esim", + "title": "9esim", + "subtitle": "让任何手机支持 eSIM,可写数据的 SIM 实体卡", + "href": "https://www.9esim.com/?coupon=XIAOSHUANGERIC2", + "icon": "/icons/9esim.webp" + } + ] + }, + { + "id": "finance", + "title": "金融收款", + "links": [ + { + "id": "wise", + "title": "wise", + "subtitle": "线上开通境外银行账户(身份证即可)", + "href": "https://wise.com/invite/amc/xiaoshuangh", + "icon": "/icons/wise.webp" + }, + { + "id": "okx", + "title": "OKX 交易所", + "subtitle": "加密货币交易所", + "href": "https://okx.com/join/84999861", + "icon": "/icons/okx.webp" + }, + { + "id": "safepal", + "title": "safepal (U卡)", + "subtitle": "邀请码 104809,出海神卡,加密货币与法币转换", + "href": "https://www.safepal.com/zh-cn/", + "icon": "/icons/safepal.webp" + }, + { + "id": "paypal", + "title": "paypal", + "subtitle": "出海收款必备,TikTok 提现、佣金提现", + "href": "https://py.pl/1tyAwY", + "icon": "/icons/paypal.webp" + }, + { + "id": "bmc", + "title": "buy me coffee", + "subtitle": "赞助收款平台", + "href": "https://www.buymeacoffee.com/invite/xiaoshuangi", + "icon": "/icons/bmc.webp" + } + ] + } + ] + }, + { + "id": "studio", + "title": "工作室", + "emoji": "☕", + "groups": [ + { + "id": "proxy", + "title": "科学上网", + "links": [ + { + "id": "jms", + "title": "just my socks", + "subtitle": "老牌网络代理服务", + "href": "https://justmysocks.net/members/aff.php?aff=12475", + "icon": "/icons/jms.webp" + }, + { + "id": "ykk", + "title": "Ykk Cloud", + "subtitle": "性价比高的网络加速服务", + "href": "https://panel.ykkk.best/#/register?code=xYQebIgT", + "icon": "/icons/ykk.webp" + } + ] + }, + { + "id": "vps", + "title": "VPS 服务器", + "links": [ + { + "id": "vultr", + "title": "vultr", + "subtitle": "可按分钟计费的服务器(可支付宝)", + "href": "https://www.vultr.com/?ref=8985003", + "icon": "/icons/vultr.webp" + }, + { + "id": "bwh", + "title": "bandwagonhost", + "subtitle": "针对中国线路优化的高质量 VPS", + "href": "https://bandwagonhost.com/aff.php?aff=25463", + "icon": "/icons/bwh.webp" + }, + { + "id": "racknerd", + "title": "RackNerd", + "subtitle": "最便宜的美国 VPS 服务器", + "href": "https://my.racknerd.com/aff.php?aff=15630", + "icon": "/icons/racknerd.webp" + }, + { + "id": "qcloud", + "title": "腾讯云", + "subtitle": "腾讯服务器,国内速度快", + "href": "https://curl.qcloud.com/BZZ2MBTN", + "icon": "/icons/qcloud.webp" + }, + { + "id": "aliyun", + "title": "阿里云", + "subtitle": "阿里巴巴旗下 VPS 服务器", + "href": "https://www.aliyun.com/minisite/goods?userCode=dii0ofzg", + "icon": "/icons/aliyun.webp" + }, + { + "id": "colocrossing", + "title": "colocrossing", + "subtitle": "美国 VPS 服务商", + "href": "https://cloud.colocrossing.com/aff.php?aff=1846", + "icon": "/icons/colocrossing.webp" + } + ] + }, + { + "id": "resi-ip", + "title": "静态住宅 IP", + "links": [ + { + "id": "iproyal", + "title": "iproyal", + "subtitle": "静态住宅 IP,出海必备", + "href": "https://iproyal.cn/?r=nomadro", + "icon": "/icons/iproyal.webp" + }, + { + "id": "proxy-cheap", + "title": "proxy-cheap", + "subtitle": "便宜的住宅 IP", + "href": "https://app.proxy-cheap.com/r/bmcMoY", + "icon": "/icons/proxy-cheap.webp" + }, + { + "id": "smartproxy", + "title": "smartproxy", + "subtitle": "静态住宅 IP 服务商", + "href": "https://www.smartproxy.org/register/?invitation_code=7RXPLM", + "icon": "/icons/smartproxy.webp" + } + ] + }, + { + "id": "resi-vps", + "title": "住宅 VPS 服务器", + "links": [ + { + "id": "lisa", + "title": "lisa", + "subtitle": "住宅 IP 服务器,TikTok 必备", + "href": "https://lisahost.com/aff.php?aff=7566", + "icon": "/icons/lisa.webp" + }, + { + "id": "voyracloud", + "title": "voyracloud", + "subtitle": "静态住宅 IP 服务器", + "href": "https://www.voyracloud.com/?ref_code=2LJPRTE5", + "icon": "/icons/voyracloud.webp" + } + ] + }, + { + "id": "hardware", + "title": "硬件", + "links": [ + { + "id": "rpi", + "title": "树莓派", + "subtitle": "无与伦比的 ARM 开发板", + "href": "https://uland.taobao.com/coupon/edetail?e=5hkWkiThOmClhHvvyUNXZfh8CuWt5YH5OVuOuRD5gLJMmdsrkidbOWBzzpT26idJPSbzO0ArF68ertbZl9wqRUbeJevxT02sHgNIxyFsmaMOsi2OxhyQb%2BhYUTTNN7GRRSHvQe2jOLZ9pbNCYX0I%2BPP%2BWUTgK%2F%2B0I%2BtaUgbudUxA%2B536asYsLWVfKa%2BhVnND4NyB1fwjO3bJLf667Yy8Q5jB6TX2HR3Qrq8LCw4Dujjqyaqc1rGLUwdQHHM8iY%2BUDMxLRSiIntldeBz90au3REAmOgZ0RtdnfeSBK3KXa0%2FA9snq37kKL6zL4bZ3EAS%2F2oYsLHTsIOxroXBFP6oz%2BA%3D%3D&traceId=213e032e17650447230113327e0c4f&union_lens=lensId%3APUB%401765044691%400b521400_0da4_19af4dca231_da77%400290EZjuZ2xbgOlH3NTYAyI%40eyJmbG9vcklkIjoyMDg3MTcxMDEwLCJzcG1CIjoiiX3BvcnRhbF92Ml90b29sX3NlbGVjdGlvbmxpc3RfZGV0YWlsX2luZGV4X2h0bSIsInNyY0Zsb29ySWQiiOiiI4MDY2NCJ9", + "icon": "/icons/rpi.webp" + }, + { + "id": "mini-pc", + "title": "迷你主机", + "subtitle": "具有性价比的 X86 小主机", + "href": "https://s.click.taobao.com/t?e=m%3D2%26s%3DCb1Quo%2F0Wb5w4vFB6t2Z2ueEDrYVVa64YUrQeSeIhnK53hKxp7mNFuhCZFSw9yq2woqx0EHqItb0JlhLk0Jl4eVWmNyHytKdTKJ80yOOA0Mtd0e5Ku7CIDk5ffgV5fRI1qrpxiwMoCNxc1AtbZGVS936JshS%2F%2FIdwgQgG5hIzyLNEPXytV9ALtCLThlbPuuZLb93Df8fOziGg7JVKZos%2BjLJJgRsfWlWtajntMnDLEHhuApO%2F8klJ62RdK%2B6%2FP7Z6k6s3rDgsC6jO9AJYjY8CXJ%2BwEVkOqHFPSJJXArlDuzso5nyPLY4NPiLqets6lIJ", + "icon": "/icons/mini-pc.webp" + }, + { + "id": "gpu", + "title": "GPU 显卡", + "subtitle": "AI 时代的算力基础设施", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATUJK1olXwEDUFZVCkkRC18IGloWWwALV19aAUonRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoUBWkBGFoSVAcdDRsBVXsQB29pbiZRKGMHKlclCEgVWghcGV91UQoyVW5dCUoWAmYPHl4RbTYCU24fZp-esLulkIK0yd6n5V1eOEonA2kPH1oQWQUFV19VCnsQA2Y4El0dWQ4LU1hBC00SCmcJK2slXjYFVFdJDjlWUXsOaWslXTYBZF5cCEoQAGoLH1kRQQYHV1lbAFcXBWgMGl4RXgUKUllYOEkWAmsBK2vL05J3IB9Uajl8UStqSz4dFQ9jitDJGTlnA2sPH0olImF5ECFaSU0VVAdNQDxuW09KFiw8SyljbW1jcih3JWNxNlwaS0hqWBhoX2sQbQEEVW4", + "icon": "/icons/gpu.webp" + } + ] + }, + { + "id": "software", + "title": "软件", + "links": [ + { + "id": "3x-ui", + "title": "3X-UI", + "subtitle": "科学上网仪表盘", + "href": "https://github.com/MHSanaei/3x-ui", + "icon": "/icons/3x-ui.webp" + }, + { + "id": "mihomo", + "title": "虚空终端 mihomo", + "subtitle": "代理软件以及配置 wiki", + "href": "https://wiki.metacubex.one/config/", + "icon": "/icons/mihomo.webp" + }, + { + "id": "frp", + "title": "frp", + "subtitle": "高性能内网穿透", + "href": "https://gofrp.org/zh-cn/docs/", + "icon": "/icons/frp.webp" + }, + { + "id": "wireguard", + "title": "wireguard", + "subtitle": "P2P 打洞组网", + "href": "https://github.com/WireGuard/wireguard-go", + "icon": "/icons/wireguard.webp" + }, + { + "id": "lineageos", + "title": "lineageos (pi5)", + "subtitle": "树莓派的安卓 LineageOS", + "href": "https://konstakang.com/devices/rpi5/", + "icon": "/icons/lineageos.webp" + } + ] + }, + { + "id": "gear", + "title": "数码装备", + "links": [ + { + "id": "ai-glasses", + "title": "AI 眼镜", + "subtitle": "第一视角拍摄", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATkJK1olXw8AXVxfCU8UA18IGloVWwcKXFlZDUgnRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoXBW4AE1wRWAUdDRsBVXtwXw1pZzNKAmZgFFwlYUh8UzVBZyZlUQoyVW5dCUoWAmYPHl4RbTYCU24fZpO9hbeBtYKjxt-jwInikZ-fjV8JK1sTWgIDUVtVAEkVA2w4HFscbQ8EXFpVAUwRH2wOHlIdXDYyZF1tD0seF2l6WgkBW3QyZF5tC3sXAm8AGFsVXAEBXEJdDUMVB24UG10SWQcHUVZbAE8fCl8KGloRVDYyitPtY0JrQAYIYj0dXGZKMVc5T5Was35jYlMVXQUTZAQOch9SZTMNbC9gIGdQJi5VCzkRSCpfWDUXVAJlDQoCbw1gcG9qRwYdHnYyUW5aDEgn", + "icon": "/icons/ai-glasses.webp" + }, + { + "id": "action-cam", + "title": "运动相机", + "subtitle": "骑行记录仪", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eCE0UB28ME1slGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWXQABUF5ZAEsIWipURmt3CFhXIQkpbChuRy8JGDpRJUJQDwkbBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVpaDEsUM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmsNHl8UXQQeVF1ZDE0UH28OHF8UWAIDUVddCkgnAW4JH1IlbdiMwDxcaAJfBTlNTRpCBHpHLhyDht8GcR8BHFIUTDZ6NSMpYRJweWtBfAUXC2JyXBkpfTNRWi1mGV4UAlgENFpeDA13cRRcTAJLbQMyU1peOA", + "icon": "/icons/action-cam.webp" + }, + { + "id": "drone", + "title": "无人机", + "subtitle": "天空视角", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eAUIeB2YMH14lGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWVA8LUFdZDE4IWipURmtCHHVEABsFVChPYRJAbVhlGRhbFRgLBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVdbC04VM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmgLHlgRXgIeVFpfDU0UH28OHF8UWAIDUlteAUwnAW4JH1IlbdiMwCQvcipMBxJhfV9HGgF3FVaDht8GfxgBGVkRTDZUCjsVbzBTYRJbeRkWGG9WAx02D0JcUy5mGRNRFEJiUwgldStzfx1wTj1GbQMyU1peOA", + "icon": "/icons/drone.webp" + } + ] + } + ] + }, + { + "id": "overseas", + "title": "出海掘金", + "emoji": "◎", + "groups": [ + { + "id": "tiktok", + "title": "TikTok", + "links": [ + { + "id": "tiktok", + "title": "TikTok", + "subtitle": "全球直播平台", + "href": "https://www.tiktok.com/", + "icon": "/icons/tiktok.webp" + }, + { + "id": "cloud-phone", + "title": "云手机", + "subtitle": "用开源硬件打造云手机", + "href": "https://www.hackrobot.cn/", + "icon": "/icons/cloud-phone.webp" + }, + { + "id": "vcam", + "title": "vcam 虚拟摄像头", + "subtitle": "Xposed 模块,替换摄像头视频流", + "href": "https://pan.baidu.com/s/1NW4GMGA3cxfUQNWZPD9vOg?pwd=live", + "icon": "/icons/vcam.webp" + } + ] + }, + { + "id": "youtube", + "title": "YouTube", + "links": [ + { + "id": "youtube", + "title": "YouTube", + "subtitle": "全球最大的视频网站", + "href": "https://www.youtube.com/", + "icon": "/icons/youtube.webp" + }, + { + "id": "smmfollows", + "title": "smmfollows", + "subtitle": "刷粉刷量平台", + "href": "https://smmfollows.com/ref/dfugq", + "icon": "/icons/smmfollows.webp" + }, + { + "id": "yt-auto-dismiss", + "title": "YouTube Studio Auto Dismiss", + "subtitle": "油猴脚本,无人直播辅助", + "href": "https://greasyfork.org/zh-CN/scripts/557378-youtube-studio-auto-dismiss", + "icon": "/icons/yt-auto-dismiss.webp" + } + ] + }, + { + "id": "web", + "title": "Web 网站", + "links": [ + { + "id": "namesilo", + "title": "namesilo", + "subtitle": "注册域名", + "href": "https://www.namesilo.com/?rid=3bd4047mc", + "icon": "/icons/namesilo.webp" + }, + { + "id": "aapanel", + "title": "aapanel", + "subtitle": "宝塔面板", + "href": "https://www.aapanel.com?referral_code=wcn04vez", + "icon": "/icons/aapanel.webp" + }, + { + "id": "cloudflare", + "title": "Cloudflare", + "subtitle": "互联网基础节点", + "href": "https://www.cloudflare.com/", + "icon": "/icons/cloudflare.webp" + }, + { + "id": "ga", + "title": "Google Analytics", + "subtitle": "网站流量统计服务", + "href": "https://analytics.google.com/", + "icon": "/icons/ga.webp" + }, + { + "id": "adsense", + "title": "Google AdSense", + "subtitle": "Google 广告服务", + "href": "https://www.google.com/adsense/", + "icon": "/icons/adsense.webp" + } + ] + } + ] + }, + { + "id": "live", + "title": "无人直播", + "emoji": "▷", + "groups": [ + { + "id": "live-soft", + "title": "软件", + "links": [ + { + "id": "obs", + "title": "OBS", + "subtitle": "免费开源:录屏 / 推流直播", + "href": "https://pan.quark.cn/s/03e89f1bbc9b", + "icon": "/icons/obs.webp" + }, + { + "id": "ffmpeg", + "title": "ffmpeg", + "subtitle": "命令行视频处理 / 推流,Python 自动化", + "href": "https://www.ffmpeg.org/download.html", + "icon": "/icons/ffmpeg.webp" + }, + { + "id": "srs", + "title": "SRS", + "subtitle": "开源直播服务器,拉流推流中转", + "href": "https://ossrs.io/", + "icon": "/icons/srs.webp" + } + ] + } + ] + }, + { + "id": "pay", + "title": "支付网关", + "emoji": "◇", + "groups": [ + { + "id": "merchant", + "title": "小微商户", + "links": [ + { + "id": "nowpayments", + "title": "NOWPayments", + "subtitle": "加密货币收款", + "href": "https://account.nowpayments.io/create-account?link_id=3680795921", + "icon": "/icons/nowpayments.webp" + }, + { + "id": "creem", + "title": "Creem", + "subtitle": "软件出海收款,税务合规", + "href": "https://www.creem.io/", + "icon": "/icons/creem.webp" + }, + { + "id": "stripe", + "title": "Stripe", + "subtitle": "全球在线支付", + "href": "https://stripe.com/", + "icon": "/icons/stripe.webp" + }, + { + "id": "zpay", + "title": "z-pay", + "subtitle": "支持独角数卡 · 易支付", + "href": "https://z-pay.cn/?uid=22119", + "icon": "/icons/zpay.webp" + }, + { + "id": "xorpay", + "title": "xorpay", + "subtitle": "支持收银台支付", + "href": "https://xorpay.com?r=nomadro", + "icon": "/icons/xorpay.webp" + } + ] + } + ] + }, + { + "id": "books", + "title": "书籍", + "emoji": "◈", + "groups": [ + { + "id": "handbook", + "title": "手册", + "links": [ + { + "id": "nomad-book", + "title": "数字游民手册", + "subtitle": "复制链接后在小红书打开", + "href": "https://www.xiaohongshu.com/s/poster?bgimg=https%3A%2F%2Fsns-redskillhub-s1.xhscdn.com%2Fred_app_image%2F1040g4m8325ke1q36l0404a4druqrlvd5valvb80%3Fsign%3Dbc0c41c2e219f6ca904f52176bd17f54%26t%3D6c9a7a85&xhsshare=&appuid=5acc75ba4eacab15c751fda5&apptime=1790527237&deeplink=xhsdiscover%3A%2F%2FminiTool%2F6ab8984da1e3af0015584e92%3Fsource%3Dh5%26page_key%3D28%26xhsMpScreenMode%3Dfull%26share_id%3D5e90e5979b054c8baf8b3db4c8993d79%26share_channel%3Dcopy_link", + "icon": "/icons/nomad-book.webp", + "action": "copy" + } + ] + } + ] + }, + { + "id": "forum", + "title": "论坛社区", + "emoji": "◉", + "groups": [ + { + "id": "blog", + "title": "博客", + "links": [ + { + "id": "yidu", + "title": "异度世界", + "subtitle": "独立博客,Hugo 搭建", + "href": "https://blog.hackrobot.cn", + "icon": "/icons/yidu.webp" + }, + { + "id": "dsx", + "title": "大师兄", + "subtitle": "AI 实践", + "href": "https://dsx2016.com", + "icon": "/icons/dsx.webp" + } + ] + } + ] + } + ], + "brandIcon": "/icons/_brand.webp" +} diff --git a/weapp/icons/1psim.webp b/weapp/icons/1psim.webp new file mode 100644 index 0000000..a64841a Binary files /dev/null and b/weapp/icons/1psim.webp differ diff --git a/weapp/icons/3x-ui.webp b/weapp/icons/3x-ui.webp new file mode 100644 index 0000000..6ef85ad Binary files /dev/null and b/weapp/icons/3x-ui.webp differ diff --git a/weapp/icons/9esim.webp b/weapp/icons/9esim.webp new file mode 100644 index 0000000..e260506 Binary files /dev/null and b/weapp/icons/9esim.webp differ diff --git a/weapp/icons/_brand.webp b/weapp/icons/_brand.webp new file mode 100644 index 0000000..9cf906b Binary files /dev/null and b/weapp/icons/_brand.webp differ diff --git a/weapp/icons/aapanel.webp b/weapp/icons/aapanel.webp new file mode 100644 index 0000000..75eabea Binary files /dev/null and b/weapp/icons/aapanel.webp differ diff --git a/weapp/icons/action-cam.webp b/weapp/icons/action-cam.webp new file mode 100644 index 0000000..91cf880 Binary files /dev/null and b/weapp/icons/action-cam.webp differ diff --git a/weapp/icons/adsense.webp b/weapp/icons/adsense.webp new file mode 100644 index 0000000..d6dcd29 Binary files /dev/null and b/weapp/icons/adsense.webp differ diff --git a/weapp/icons/ai-glasses.webp b/weapp/icons/ai-glasses.webp new file mode 100644 index 0000000..63c57a1 Binary files /dev/null and b/weapp/icons/ai-glasses.webp differ diff --git a/weapp/icons/aliyun.webp b/weapp/icons/aliyun.webp new file mode 100644 index 0000000..d6fac1a Binary files /dev/null and b/weapp/icons/aliyun.webp differ diff --git a/weapp/icons/bmc.webp b/weapp/icons/bmc.webp new file mode 100644 index 0000000..f151a5d Binary files /dev/null and b/weapp/icons/bmc.webp differ diff --git a/weapp/icons/bwh.webp b/weapp/icons/bwh.webp new file mode 100644 index 0000000..265aaa0 Binary files /dev/null and b/weapp/icons/bwh.webp differ diff --git a/weapp/icons/chatgpt.webp b/weapp/icons/chatgpt.webp new file mode 100644 index 0000000..4d86031 Binary files /dev/null and b/weapp/icons/chatgpt.webp differ diff --git a/weapp/icons/cloud-phone.webp b/weapp/icons/cloud-phone.webp new file mode 100644 index 0000000..34036ed Binary files /dev/null and b/weapp/icons/cloud-phone.webp differ diff --git a/weapp/icons/cloudflare.webp b/weapp/icons/cloudflare.webp new file mode 100644 index 0000000..8de0f5a Binary files /dev/null and b/weapp/icons/cloudflare.webp differ diff --git a/weapp/icons/colocrossing.webp b/weapp/icons/colocrossing.webp new file mode 100644 index 0000000..06dd41c Binary files /dev/null and b/weapp/icons/colocrossing.webp differ diff --git a/weapp/icons/creem.webp b/weapp/icons/creem.webp new file mode 100644 index 0000000..f8b751e Binary files /dev/null and b/weapp/icons/creem.webp differ diff --git a/weapp/icons/drone.webp b/weapp/icons/drone.webp new file mode 100644 index 0000000..edc61f8 Binary files /dev/null and b/weapp/icons/drone.webp differ diff --git a/weapp/icons/dsx.webp b/weapp/icons/dsx.webp new file mode 100644 index 0000000..de1e117 Binary files /dev/null and b/weapp/icons/dsx.webp differ diff --git a/weapp/icons/ffmpeg.webp b/weapp/icons/ffmpeg.webp new file mode 100644 index 0000000..8599db3 Binary files /dev/null and b/weapp/icons/ffmpeg.webp differ diff --git a/weapp/icons/frp.webp b/weapp/icons/frp.webp new file mode 100644 index 0000000..c6547fe Binary files /dev/null and b/weapp/icons/frp.webp differ diff --git a/weapp/icons/ga.webp b/weapp/icons/ga.webp new file mode 100644 index 0000000..4bc3288 Binary files /dev/null and b/weapp/icons/ga.webp differ diff --git a/weapp/icons/gpu.webp b/weapp/icons/gpu.webp new file mode 100644 index 0000000..4e0757e Binary files /dev/null and b/weapp/icons/gpu.webp differ diff --git a/weapp/icons/grok.webp b/weapp/icons/grok.webp new file mode 100644 index 0000000..2498a74 Binary files /dev/null and b/weapp/icons/grok.webp differ diff --git a/weapp/icons/iproyal.webp b/weapp/icons/iproyal.webp new file mode 100644 index 0000000..22747a1 Binary files /dev/null and b/weapp/icons/iproyal.webp differ diff --git a/weapp/icons/jms.webp b/weapp/icons/jms.webp new file mode 100644 index 0000000..ee44874 Binary files /dev/null and b/weapp/icons/jms.webp differ diff --git a/weapp/icons/lineageos.webp b/weapp/icons/lineageos.webp new file mode 100644 index 0000000..300499d Binary files /dev/null and b/weapp/icons/lineageos.webp differ diff --git a/weapp/icons/lisa.webp b/weapp/icons/lisa.webp new file mode 100644 index 0000000..ada3df6 Binary files /dev/null and b/weapp/icons/lisa.webp differ diff --git a/weapp/icons/mihomo.webp b/weapp/icons/mihomo.webp new file mode 100644 index 0000000..75708e5 Binary files /dev/null and b/weapp/icons/mihomo.webp differ diff --git a/weapp/icons/mini-pc.webp b/weapp/icons/mini-pc.webp new file mode 100644 index 0000000..f3038b0 Binary files /dev/null and b/weapp/icons/mini-pc.webp differ diff --git a/weapp/icons/namesilo.webp b/weapp/icons/namesilo.webp new file mode 100644 index 0000000..f29a994 Binary files /dev/null and b/weapp/icons/namesilo.webp differ diff --git a/weapp/icons/nomad-book.webp b/weapp/icons/nomad-book.webp new file mode 100644 index 0000000..b495988 Binary files /dev/null and b/weapp/icons/nomad-book.webp differ diff --git a/weapp/icons/nomadcna.webp b/weapp/icons/nomadcna.webp new file mode 100644 index 0000000..285782e Binary files /dev/null and b/weapp/icons/nomadcna.webp differ diff --git a/weapp/icons/nomadlist.webp b/weapp/icons/nomadlist.webp new file mode 100644 index 0000000..640d92f Binary files /dev/null and b/weapp/icons/nomadlist.webp differ diff --git a/weapp/icons/nomadro.webp b/weapp/icons/nomadro.webp new file mode 100644 index 0000000..39b692f Binary files /dev/null and b/weapp/icons/nomadro.webp differ diff --git a/weapp/icons/nowpayments.webp b/weapp/icons/nowpayments.webp new file mode 100644 index 0000000..e042076 Binary files /dev/null and b/weapp/icons/nowpayments.webp differ diff --git a/weapp/icons/obs.webp b/weapp/icons/obs.webp new file mode 100644 index 0000000..2fa929f Binary files /dev/null and b/weapp/icons/obs.webp differ diff --git a/weapp/icons/okx.webp b/weapp/icons/okx.webp new file mode 100644 index 0000000..85aa2e3 Binary files /dev/null and b/weapp/icons/okx.webp differ diff --git a/weapp/icons/paypal.webp b/weapp/icons/paypal.webp new file mode 100644 index 0000000..3cc416e Binary files /dev/null and b/weapp/icons/paypal.webp differ diff --git a/weapp/icons/proxy-cheap.webp b/weapp/icons/proxy-cheap.webp new file mode 100644 index 0000000..7483870 Binary files /dev/null and b/weapp/icons/proxy-cheap.webp differ diff --git a/weapp/icons/qcloud.webp b/weapp/icons/qcloud.webp new file mode 100644 index 0000000..53a5430 Binary files /dev/null and b/weapp/icons/qcloud.webp differ diff --git a/weapp/icons/racknerd.webp b/weapp/icons/racknerd.webp new file mode 100644 index 0000000..2262856 Binary files /dev/null and b/weapp/icons/racknerd.webp differ diff --git a/weapp/icons/rpi.webp b/weapp/icons/rpi.webp new file mode 100644 index 0000000..cc6268a Binary files /dev/null and b/weapp/icons/rpi.webp differ diff --git a/weapp/icons/safepal.webp b/weapp/icons/safepal.webp new file mode 100644 index 0000000..570274e Binary files /dev/null and b/weapp/icons/safepal.webp differ diff --git a/weapp/icons/smartproxy.webp b/weapp/icons/smartproxy.webp new file mode 100644 index 0000000..96ba0cf Binary files /dev/null and b/weapp/icons/smartproxy.webp differ diff --git a/weapp/icons/smmfollows.webp b/weapp/icons/smmfollows.webp new file mode 100644 index 0000000..7e39e92 Binary files /dev/null and b/weapp/icons/smmfollows.webp differ diff --git a/weapp/icons/srs.webp b/weapp/icons/srs.webp new file mode 100644 index 0000000..ee1c4a8 Binary files /dev/null and b/weapp/icons/srs.webp differ diff --git a/weapp/icons/stripe.webp b/weapp/icons/stripe.webp new file mode 100644 index 0000000..829ef26 Binary files /dev/null and b/weapp/icons/stripe.webp differ diff --git a/weapp/icons/tello.webp b/weapp/icons/tello.webp new file mode 100644 index 0000000..9ec43f3 Binary files /dev/null and b/weapp/icons/tello.webp differ diff --git a/weapp/icons/tiktok.webp b/weapp/icons/tiktok.webp new file mode 100644 index 0000000..d66705e Binary files /dev/null and b/weapp/icons/tiktok.webp differ diff --git a/weapp/icons/vcam.webp b/weapp/icons/vcam.webp new file mode 100644 index 0000000..66c1003 Binary files /dev/null and b/weapp/icons/vcam.webp differ diff --git a/weapp/icons/voyracloud.webp b/weapp/icons/voyracloud.webp new file mode 100644 index 0000000..077e0b5 Binary files /dev/null and b/weapp/icons/voyracloud.webp differ diff --git a/weapp/icons/vultr.webp b/weapp/icons/vultr.webp new file mode 100644 index 0000000..a6caed2 Binary files /dev/null and b/weapp/icons/vultr.webp differ diff --git a/weapp/icons/wireguard.webp b/weapp/icons/wireguard.webp new file mode 100644 index 0000000..df6acdd Binary files /dev/null and b/weapp/icons/wireguard.webp differ diff --git a/weapp/icons/wise.webp b/weapp/icons/wise.webp new file mode 100644 index 0000000..429ae7c Binary files /dev/null and b/weapp/icons/wise.webp differ diff --git a/weapp/icons/xorpay.webp b/weapp/icons/xorpay.webp new file mode 100644 index 0000000..b20e15a Binary files /dev/null and b/weapp/icons/xorpay.webp differ diff --git a/weapp/icons/yesim.webp b/weapp/icons/yesim.webp new file mode 100644 index 0000000..be7ae90 Binary files /dev/null and b/weapp/icons/yesim.webp differ diff --git a/weapp/icons/yidu.webp b/weapp/icons/yidu.webp new file mode 100644 index 0000000..4de6f0d Binary files /dev/null and b/weapp/icons/yidu.webp differ diff --git a/weapp/icons/ykk.webp b/weapp/icons/ykk.webp new file mode 100644 index 0000000..fb18929 Binary files /dev/null and b/weapp/icons/ykk.webp differ diff --git a/weapp/icons/youtube.webp b/weapp/icons/youtube.webp new file mode 100644 index 0000000..5ee5b72 Binary files /dev/null and b/weapp/icons/youtube.webp differ diff --git a/weapp/icons/yt-auto-dismiss.webp b/weapp/icons/yt-auto-dismiss.webp new file mode 100644 index 0000000..b575d7e Binary files /dev/null and b/weapp/icons/yt-auto-dismiss.webp differ diff --git a/weapp/icons/zpay.webp b/weapp/icons/zpay.webp new file mode 100644 index 0000000..3dbbd06 Binary files /dev/null and b/weapp/icons/zpay.webp differ diff --git a/weapp/pages/index/index.js b/weapp/pages/index/index.js new file mode 100644 index 0000000..f29402c --- /dev/null +++ b/weapp/pages/index/index.js @@ -0,0 +1,214 @@ +const nav = require("../../data/links.json"); + +function countLinks(cat) { + let n = 0; + const groups = cat.groups || []; + for (let i = 0; i < groups.length; i++) { + n += (groups[i].links || []).length; + } + return n; +} + +function withCounts(categories) { + return (categories || []).map((c) => + Object.assign({}, c, { linkCount: countLinks(c) }) + ); +} + +Page({ + data: { + brand: nav.brand || "游民导航", + tagline: nav.tagline || "", + footer: nav.footer || "", + brandIcon: nav.brandIcon || "/icons/_brand.webp", + categories: withCounts(nav.categories), + activeId: (nav.categories && nav.categories[0] && nav.categories[0].id) || "", + activeTitle: + (nav.categories && nav.categories[0] && nav.categories[0].title) || "", + theme: "light", + drawerOpen: false, + showTop: false, + toast: "", + statusBarHeight: 20, + navBarHeight: 44, + sheetOpen: false, + sheetLink: null, + }, + + _jumping: false, + _sectionTops: [], + _toastTimer: null, + + onLoad() { + const sys = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); + const statusBarHeight = sys.statusBarHeight || 20; + const menu = wx.getMenuButtonBoundingClientRect + ? wx.getMenuButtonBoundingClientRect() + : null; + let navBarHeight = 44; + if (menu && menu.height) { + navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height; + } + + const app = getApp(); + const theme = (app && app.globalData && app.globalData.theme) || "light"; + + this.setData({ + statusBarHeight, + navBarHeight, + theme, + }); + this._applyTheme(theme); + }, + + onReady() { + wx.nextTick(() => this._measureSections()); + }, + + onShow() { + this._applyTheme(this.data.theme); + }, + + onPageScroll(e) { + const scrollTop = e.scrollTop || 0; + const showTop = scrollTop > 480; + if (showTop !== this.data.showTop) { + this.setData({ showTop }); + } + if (this._jumping || !this._sectionTops.length) return; + + const marker = scrollTop + this.data.statusBarHeight + this.data.navBarHeight + 24; + let current = this._sectionTops[0].id; + for (let i = 0; i < this._sectionTops.length; i++) { + if (this._sectionTops[i].top <= marker) current = this._sectionTops[i].id; + else break; + } + if (current !== this.data.activeId) { + const cat = this.data.categories.find((c) => c.id === current); + this.setData({ + activeId: current, + activeTitle: (cat && cat.title) || "", + }); + } + }, + + _measureSections() { + const query = wx.createSelectorQuery(); + query.selectAll(".cat-panel").boundingClientRect(); + query.selectViewport().scrollOffset(); + query.exec((res) => { + const rects = (res && res[0]) || []; + const scroll = (res && res[1]) || { scrollTop: 0 }; + this._sectionTops = rects.map((r) => ({ + id: (r.id || "").replace(/^cat-/, ""), + top: (r.top || 0) + (scroll.scrollTop || 0), + })); + }); + }, + + _applyTheme(theme) { + const dark = theme === "dark"; + wx.setNavigationBarColor({ + frontColor: dark ? "#ffffff" : "#000000", + backgroundColor: dark ? "#0a0f14" : "#e9eef4", + animation: { duration: 200, timingFunc: "easeIn" }, + }); + wx.setBackgroundColor({ + backgroundColor: dark ? "#0a0f14" : "#e9eef4", + backgroundColorTop: dark ? "#0a0f14" : "#e9eef4", + backgroundColorBottom: dark ? "#0a0f14" : "#e9eef4", + }); + }, + + openDrawer() { + this.setData({ drawerOpen: true }); + }, + + closeDrawer() { + this.setData({ drawerOpen: false }); + }, + + noop() {}, + + toggleTheme() { + const theme = this.data.theme === "dark" ? "light" : "dark"; + this.setData({ theme }); + try { + wx.setStorageSync("navmini-theme", theme); + } catch (e) { + /* ignore */ + } + const app = getApp(); + if (app && app.globalData) app.globalData.theme = theme; + this._applyTheme(theme); + }, + + onSelectCategory(e) { + const id = e.currentTarget.dataset.id; + if (!id) return; + const cat = this.data.categories.find((c) => c.id === id); + this.setData({ + drawerOpen: false, + activeId: id, + activeTitle: (cat && cat.title) || "", + }); + this._jumping = true; + wx.pageScrollTo({ + selector: `#cat-${id}`, + duration: 320, + offsetTop: -(this.data.statusBarHeight + this.data.navBarHeight + 8), + complete: () => { + setTimeout(() => { + this._jumping = false; + this._measureSections(); + }, 360); + }, + }); + }, + + onTapLink(e) { + const { id, catid, groupid } = e.currentTarget.dataset; + let link = null; + const cat = this.data.categories.find((c) => c.id === catid); + if (cat) { + const group = (cat.groups || []).find((g) => g.id === groupid); + if (group) link = (group.links || []).find((l) => l.id === id); + } + if (!link || !link.href) { + this._showToast("无效链接"); + return; + } + this.setData({ sheetOpen: true, sheetLink: link }); + }, + + closeSheet() { + this.setData({ sheetOpen: false, sheetLink: null }); + }, + + copySheetUrl() { + const link = this.data.sheetLink; + if (!link || !link.href) return; + wx.setClipboardData({ + data: link.href, + success: () => { + this._showToast("已复制,请到浏览器打开"); + }, + fail: () => { + this._showToast("复制失败"); + }, + }); + }, + + backTop() { + wx.pageScrollTo({ scrollTop: 0, duration: 300 }); + }, + + _showToast(msg) { + if (this._toastTimer) clearTimeout(this._toastTimer); + this.setData({ toast: msg }); + this._toastTimer = setTimeout(() => { + this.setData({ toast: "" }); + this._toastTimer = null; + }, 1800); + }, +}); diff --git a/weapp/pages/index/index.json b/weapp/pages/index/index.json new file mode 100644 index 0000000..b16ef3e --- /dev/null +++ b/weapp/pages/index/index.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "usingComponents": {} +} diff --git a/weapp/pages/index/index.wxml b/weapp/pages/index/index.wxml new file mode 100644 index 0000000..e2a3ab5 --- /dev/null +++ b/weapp/pages/index/index.wxml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + {{brand}} + {{activeTitle}} + + + + + {{theme === 'dark' ? '☀' : '☾'}} + + + + + + + + + {{item.title}} + {{item.linkCount}} + + + + + {{group.title}} + + + + + + + {{link.title}} + {{link.subtitle}} + + + + + + + + + {{footer}} + + + + + + + + + + + {{brand}} + {{tagline}} + + + + × + + + + + {{item.emoji}} + + {{item.title}} + {{item.linkCount}} 个入口 + + + + + + + + + + + + + + {{sheetLink.title}} + {{sheetLink.subtitle}} + + + 网址 + {{sheetLink.href}} + + + + + 小程序无法直接打开外链,复制后到浏览器粘贴打开 + + + + + ↑ + + + {{toast}} + diff --git a/weapp/pages/index/index.wxss b/weapp/pages/index/index.wxss new file mode 100644 index 0000000..07c55dc --- /dev/null +++ b/weapp/pages/index/index.wxss @@ -0,0 +1,539 @@ +.topbar { + position: fixed; + left: 0; + right: 0; + top: 0; + z-index: 40; + background: var(--topbar-bg); + backdrop-filter: blur(16px); + border-bottom: 1rpx solid var(--line); +} + +.topbar-inner { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20rpx 0 12rpx; + gap: 12rpx; +} + +.icon-btn { + width: 72rpx; + height: 72rpx; + display: flex; + align-items: center; + justify-content: center; + border-radius: 20rpx; + flex-shrink: 0; +} + +.icon-btn-hover { + background: var(--chip); +} + +.menu-bars { + width: 36rpx; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.menu-bars .bar { + height: 4rpx; + border-radius: 4rpx; + background: var(--ink); +} + +.menu-bars .bar.short { + width: 70%; +} + +.theme-ico, +.close-ico { + font-size: 36rpx; + line-height: 1; + color: var(--ink); +} + +.close-ico { + font-size: 48rpx; + font-weight: 300; +} + +.brand-mark { + flex: 1; + display: flex; + align-items: center; + gap: 14rpx; + min-width: 0; +} + +.brand-logo { + width: 56rpx; + height: 56rpx; + border-radius: 14rpx; + flex-shrink: 0; + background: var(--surface-solid); +} + +.brand-text { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 4rpx; + min-width: 0; +} + +.brand-name { + font-size: 30rpx; + font-weight: 700; + color: var(--ink); + line-height: 1.2; + letter-spacing: -0.02em; +} + +.brand-current { + font-size: 22rpx; + font-weight: 600; + color: var(--accent); + line-height: 1.25; + letter-spacing: -0.01em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +.page { + padding: 16rpx 28rpx 120rpx; + max-width: 1120rpx; + margin: 0 auto; +} + +.cat-panel { + margin-top: 8rpx; + padding-top: 20rpx; +} + +.cat-heading { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16rpx; + margin-bottom: 18rpx; + padding: 0 4rpx; +} + +.cat-title { + font-size: 34rpx; + font-weight: 800; + color: var(--ink); +} + +.cat-count { + font-size: 22rpx; + font-weight: 700; + color: var(--ink-faint); + background: var(--chip); + padding: 4rpx 14rpx; + border-radius: 999rpx; +} + +.group-block { + margin-bottom: 22rpx; +} + +.group-title { + display: block; + font-size: 24rpx; + font-weight: 700; + color: var(--ink-muted); + margin: 0 4rpx 14rpx; +} + +.link-grid { + display: flex; + flex-wrap: wrap; + gap: 12rpx; +} + +.link-card { + width: calc((100% - 24rpx) / 3); + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12rpx; + padding: 18rpx 14rpx; + border-radius: 22rpx; + background: var(--surface-solid); + box-shadow: var(--shadow-card); + border: 1rpx solid var(--line); +} + +.link-card-hover { + opacity: 0.88; + transform: scale(0.98); +} + +.link-icon-wrap { + width: 56rpx; + height: 56rpx; + border-radius: 16rpx; + background: var(--chip); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.link-icon { + width: 40rpx; + height: 40rpx; +} + +.link-meta { + display: flex; + flex-direction: column; + gap: 4rpx; + min-width: 0; + width: 100%; +} + +.link-title { + font-size: 24rpx; + font-weight: 700; + color: var(--ink); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.link-desc { + font-size: 20rpx; + color: var(--ink-faint); + line-height: 1.35; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} + +.footer { + margin-top: 48rpx; + text-align: center; + font-size: 22rpx; + color: var(--ink-faint); + padding-bottom: env(safe-area-inset-bottom); +} + +/* Drawer */ +.drawer-root { + position: fixed; + inset: 0; + z-index: 60; + pointer-events: none; + visibility: hidden; +} + +.drawer-root.open { + pointer-events: auto; + visibility: visible; +} + +.drawer-mask { + position: absolute; + inset: 0; + background: rgba(10, 15, 20, 0.45); + opacity: 0; + transition: opacity 0.25s ease; +} + +.drawer-root.open .drawer-mask { + opacity: 1; +} + +.drawer-panel { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: min(78vw, 560rpx); + background: var(--bg-elevated); + box-shadow: var(--shadow); + transform: translateX(-105%); + transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1); + display: flex; + flex-direction: column; +} + +.drawer-root.open .drawer-panel { + transform: translateX(0); +} + +.drawer-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12rpx; + padding: 16rpx 20rpx 20rpx; + border-bottom: 1rpx solid var(--line); +} + +.drawer-brand-wrap { + display: flex; + align-items: center; + gap: 14rpx; + min-width: 0; + padding-top: 8rpx; +} + +.drawer-logo { + width: 56rpx; + height: 56rpx; + border-radius: 14rpx; + background: var(--surface-solid); + flex-shrink: 0; +} + +.drawer-brand { + display: block; + font-size: 30rpx; + font-weight: 800; + color: var(--ink); +} + +.drawer-sub { + display: block; + margin-top: 4rpx; + font-size: 22rpx; + color: var(--ink-muted); + line-height: 1.35; +} + +.drawer-nav { + flex: 1; + height: 0; + padding: 16rpx 14rpx 40rpx; +} + +.drawer-item { + display: flex; + align-items: center; + gap: 16rpx; + padding: 20rpx 18rpx; + border-radius: 18rpx; + margin-bottom: 8rpx; +} + +.drawer-item-hover { + background: var(--chip); +} + +.drawer-item.active { + background: var(--chip-active); +} + +.drawer-item.active .drawer-item-title, +.drawer-item.active .drawer-item-count, +.drawer-item.active .drawer-emoji { + color: var(--chip-active-ink); +} + +.drawer-emoji { + font-size: 28rpx; + width: 40rpx; + text-align: center; + flex-shrink: 0; +} + +.drawer-item-text { + min-width: 0; + display: flex; + flex-direction: column; + gap: 4rpx; +} + +.drawer-item-title { + font-size: 28rpx; + font-weight: 700; + color: var(--ink); +} + +.drawer-item-count { + font-size: 22rpx; + color: var(--ink-faint); +} + +/* Sheet */ +.sheet-root { + position: fixed; + inset: 0; + z-index: 70; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.sheet-mask { + position: absolute; + inset: 0; + background: rgba(10, 15, 20, 0.5); +} + +.sheet-panel { + position: relative; + width: 100%; + max-width: 1120rpx; + background: var(--surface-solid); + border-radius: 28rpx 28rpx 0 0; + padding: 32rpx 28rpx calc(28rpx + env(safe-area-inset-bottom)); + box-shadow: var(--shadow); +} + +.sheet-head { + display: flex; + align-items: center; + gap: 18rpx; + margin-bottom: 24rpx; +} + +.sheet-icon { + width: 72rpx; + height: 72rpx; + border-radius: 18rpx; + background: var(--chip); + flex-shrink: 0; +} + +.sheet-titles { + min-width: 0; + display: flex; + flex-direction: column; + gap: 6rpx; +} + +.sheet-title { + font-size: 32rpx; + font-weight: 800; + color: var(--ink); +} + +.sheet-sub { + font-size: 24rpx; + color: var(--ink-muted); + line-height: 1.4; +} + +.sheet-label { + font-size: 22rpx; + font-weight: 700; + color: var(--ink-faint); + letter-spacing: 0.04em; + text-transform: uppercase; + margin-bottom: 10rpx; +} + +.sheet-url { + padding: 20rpx 18rpx; + border-radius: 18rpx; + background: var(--accent-soft); + border: 2rpx solid rgba(13, 143, 123, 0.35); + font-size: 24rpx; + color: var(--ink); + word-break: break-all; + line-height: 1.5; + user-select: text; +} + +.sheet-actions { + display: flex; + gap: 14rpx; + margin-top: 24rpx; +} + +.sheet-btn { + flex: 1; + min-height: 88rpx; + border-radius: 18rpx; + border: 1rpx solid var(--line) !important; + background: var(--chip) !important; + font-size: 28rpx !important; + font-weight: 700; + color: var(--ink) !important; + display: flex; + align-items: center; + justify-content: center; +} + +.sheet-btn.primary { + flex: 1.6; + background: linear-gradient(180deg, #12a08a, var(--accent-deep)) !important; + color: #fff !important; + border-color: transparent !important; +} + +.sheet-hint { + margin-top: 18rpx; + font-size: 22rpx; + color: var(--ink-faint); + text-align: center; + line-height: 1.4; +} + +.back-top { + position: fixed; + right: 28rpx; + bottom: calc(48rpx + env(safe-area-inset-bottom)); + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background: var(--surface-solid); + box-shadow: var(--shadow); + border: 1rpx solid var(--line); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transform: translateY(12rpx); + transition: opacity 0.2s ease, transform 0.2s ease; + z-index: 35; + font-size: 32rpx; + font-weight: 700; + color: var(--ink); +} + +.back-top.show { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.back-top-hover { + opacity: 0.85; +} + +.toast { + position: fixed; + left: 50%; + bottom: calc(56rpx + env(safe-area-inset-bottom)); + transform: translateX(-50%) translateY(16rpx); + padding: 14rpx 28rpx; + border-radius: 999rpx; + background: var(--toast-bg); + color: var(--toast-ink); + font-size: 24rpx; + font-weight: 600; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease; + z-index: 80; + max-width: 80vw; + text-align: center; +} + +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} diff --git a/weapp/project.config.json b/weapp/project.config.json new file mode 100644 index 0000000..65b0504 --- /dev/null +++ b/weapp/project.config.json @@ -0,0 +1,39 @@ +{ + "description": "游民导航 · 微信小程序", + "packOptions": { + "ignore": ["scripts/**", "README.md", "**/*.md"], + "include": [] + }, + "setting": { + "urlCheck": false, + "es6": true, + "enhance": true, + "postcss": true, + "minified": true, + "minifyWXSS": true, + "minifyWXML": true, + "compileWorklet": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "packNpmManually": false, + "packNpmRelationList": [], + "localPlugins": false, + "disableUseStrict": false, + "useCompilerPlugins": false, + "condition": false, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + } + }, + "compileType": "miniprogram", + "libVersion": "3.7.12", + "appid": "touristappid", + "projectname": "youmin-nav", + "condition": {}, + "editorSetting": { + "tabIndent": "insertSpaces", + "tabSize": 2 + } +} diff --git a/weapp/project.private.config.json b/weapp/project.private.config.json new file mode 100644 index 0000000..c242b09 --- /dev/null +++ b/weapp/project.private.config.json @@ -0,0 +1,14 @@ +{ + "description": "本地私有配置,勿提交敏感信息", + "projectname": "youmin-nav", + "setting": { + "urlCheck": false, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "showShadowRootInWxmlPanel": true, + "compileHotReLoad": true + } +} diff --git a/weapp/scripts/sync_data.py b/weapp/scripts/sync_data.py new file mode 100644 index 0000000..401cb82 --- /dev/null +++ b/weapp/scripts/sync_data.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Sync H5 links.json + icons into weapp/ for WeChat DevTools.""" +from __future__ import annotations + +import json +import shutil +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] # weapp/ +PROJ = ROOT.parent # navmini/ +SRC_JSON = PROJ / "src" / "data" / "links.json" +SRC_ICONS = PROJ / "public" / "icons" +OUT_JSON = ROOT / "data" / "links.json" +OUT_ICONS = ROOT / "icons" + + +def main() -> int: + if not SRC_JSON.is_file(): + print(f"missing {SRC_JSON}", file=sys.stderr) + return 1 + + data = json.loads(SRC_JSON.read_text(encoding="utf-8")) + OUT_ICONS.mkdir(parents=True, exist_ok=True) + OUT_JSON.parent.mkdir(parents=True, exist_ok=True) + + for old in OUT_ICONS.glob("*"): + if old.is_file(): + old.unlink() + + needed: list[str] = ["_brand"] + for cat in data.get("categories", []): + for group in cat.get("groups", []): + for link in group.get("links", []): + lid = link["id"] + needed.append(lid) + # 小程序本地路径(相对小程序根目录) + link["icon"] = f"/icons/{lid}.webp" + + missing = [] + for lid in needed: + src = SRC_ICONS / f"{lid}.webp" + dst = OUT_ICONS / f"{lid}.webp" + if not src.is_file(): + missing.append(lid) + continue + shutil.copy2(src, dst) + + if missing: + print("warn missing icons:", ", ".join(missing), file=sys.stderr) + + data["brandIcon"] = "/icons/_brand.webp" + + OUT_JSON.write_text( + json.dumps(data, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + icon_count = len(list(OUT_ICONS.glob("*.webp"))) + print(f"synced data/links.json + {icon_count} icons -> {ROOT}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/weapp/sitemap.json b/weapp/sitemap.json new file mode 100644 index 0000000..fd4f201 --- /dev/null +++ b/weapp/sitemap.json @@ -0,0 +1,9 @@ +{ + "desc": "游民导航", + "rules": [ + { + "action": "allow", + "page": "*" + } + ] +} diff --git a/xhs/README.md b/xhs/README.md new file mode 100644 index 0000000..37415b1 --- /dev/null +++ b/xhs/README.md @@ -0,0 +1,56 @@ +# 游民导航 · 小红书小工具 + +纯离线小红书小工具(对齐 [redmini](../../redmini) 打包规范)。**不能联网、不能打开外链、也不能程序写入剪贴板**(官方禁用 `navigator.clipboard` / `execCommand('copy')`)。 + +点击入口会: + +1. **底部弹窗**展示标题 / 简介 / **完整网址** +2. **自动选中网址**(或点「选中网址」),引导用户**手动复制** + +数据与图标同步自仓库 H5:`src/data/links.json`、`public/icons/`。 + +## 约束(官方) + +| 项 | 说明 | +|----|------| +| 网络 | 禁止 `fetch` / CDN / 外链资源 | +| 脚本 | 仅经典 ` + + diff --git a/xhs/tool/nav-data.js b/xhs/tool/nav-data.js new file mode 100644 index 0000000..f85b05c --- /dev/null +++ b/xhs/tool/nav-data.js @@ -0,0 +1,587 @@ +/* generated by scripts/sync_data.py — do not edit */ +var NAV_DATA = { + "brand": "游民导航", + "tagline": "数字游民工具与出海资源合集", + "footer": "2026 游民导航", + "categories": [ + { + "id": "nomad", + "title": "数字游民", + "emoji": "✦", + "groups": [ + { + "id": "community", + "title": "游民社区", + "links": [ + { + "id": "nomadro", + "title": "nomadro", + "subtitle": "出海导航", + "href": "https://nomadro.com", + "icon": "./icons/nomadro.webp" + }, + { + "id": "nomadlist", + "title": "nomadlist", + "subtitle": "海外最大的数字游民社区", + "href": "https://nomads.com?ref=https://nomadro.com", + "icon": "./icons/nomadlist.webp" + }, + { + "id": "nomadcna", + "title": "nomadcna", + "subtitle": "国内数字游民社区", + "href": "https://meetup.hackrobot.cn", + "icon": "./icons/nomadcna.webp" + } + ] + }, + { + "id": "toolbox", + "title": "工具箱", + "links": [ + { + "id": "chatgpt", + "title": "ChatGPT", + "subtitle": "OpenAI 开发的人工智能聊天机器人", + "href": "https://chat.openai.com/", + "icon": "./icons/chatgpt.webp" + }, + { + "id": "grok", + "title": "Grok", + "subtitle": "xAI 免费 AI 助手,追求真实与客观", + "href": "https://grok.com/", + "icon": "./icons/grok.webp" + } + ] + }, + { + "id": "us-phone", + "title": "美国手机号", + "links": [ + { + "id": "1psim", + "title": "1psim", + "subtitle": "低成本 2.5$/m,支持 eSIM 和实体卡(邮寄中国)", + "href": "https://1psim.com/7GARRL", + "icon": "./icons/1psim.webp" + }, + { + "id": "yesim", + "title": "yesim", + "subtitle": "美国手机号,3$/m", + "href": "https://yesim.app/?partner_id=1948", + "icon": "./icons/yesim.webp" + }, + { + "id": "tello", + "title": "tello", + "subtitle": "最低 5$/m,支持 eSIM", + "href": "https://tello.com/", + "icon": "./icons/tello.webp" + }, + { + "id": "9esim", + "title": "9esim", + "subtitle": "让任何手机支持 eSIM,可写数据的 SIM 实体卡", + "href": "https://www.9esim.com/?coupon=XIAOSHUANGERIC2", + "icon": "./icons/9esim.webp" + } + ] + }, + { + "id": "finance", + "title": "金融收款", + "links": [ + { + "id": "wise", + "title": "wise", + "subtitle": "线上开通境外银行账户(身份证即可)", + "href": "https://wise.com/invite/amc/xiaoshuangh", + "icon": "./icons/wise.webp" + }, + { + "id": "okx", + "title": "OKX 交易所", + "subtitle": "加密货币交易所", + "href": "https://okx.com/join/84999861", + "icon": "./icons/okx.webp" + }, + { + "id": "safepal", + "title": "safepal (U卡)", + "subtitle": "邀请码 104809,出海神卡,加密货币与法币转换", + "href": "https://www.safepal.com/zh-cn/", + "icon": "./icons/safepal.webp" + }, + { + "id": "paypal", + "title": "paypal", + "subtitle": "出海收款必备,TikTok 提现、佣金提现", + "href": "https://py.pl/1tyAwY", + "icon": "./icons/paypal.webp" + }, + { + "id": "bmc", + "title": "buy me coffee", + "subtitle": "赞助收款平台", + "href": "https://www.buymeacoffee.com/invite/xiaoshuangi", + "icon": "./icons/bmc.webp" + } + ] + } + ] + }, + { + "id": "studio", + "title": "工作室", + "emoji": "☕", + "groups": [ + { + "id": "proxy", + "title": "科学上网", + "links": [ + { + "id": "jms", + "title": "just my socks", + "subtitle": "老牌网络代理服务", + "href": "https://justmysocks.net/members/aff.php?aff=12475", + "icon": "./icons/jms.webp" + }, + { + "id": "ykk", + "title": "Ykk Cloud", + "subtitle": "性价比高的网络加速服务", + "href": "https://panel.ykkk.best/#/register?code=xYQebIgT", + "icon": "./icons/ykk.webp" + } + ] + }, + { + "id": "vps", + "title": "VPS 服务器", + "links": [ + { + "id": "vultr", + "title": "vultr", + "subtitle": "可按分钟计费的服务器(可支付宝)", + "href": "https://www.vultr.com/?ref=8985003", + "icon": "./icons/vultr.webp" + }, + { + "id": "bwh", + "title": "bandwagonhost", + "subtitle": "针对中国线路优化的高质量 VPS", + "href": "https://bandwagonhost.com/aff.php?aff=25463", + "icon": "./icons/bwh.webp" + }, + { + "id": "racknerd", + "title": "RackNerd", + "subtitle": "最便宜的美国 VPS 服务器", + "href": "https://my.racknerd.com/aff.php?aff=15630", + "icon": "./icons/racknerd.webp" + }, + { + "id": "qcloud", + "title": "腾讯云", + "subtitle": "腾讯服务器,国内速度快", + "href": "https://curl.qcloud.com/BZZ2MBTN", + "icon": "./icons/qcloud.webp" + }, + { + "id": "aliyun", + "title": "阿里云", + "subtitle": "阿里巴巴旗下 VPS 服务器", + "href": "https://www.aliyun.com/minisite/goods?userCode=dii0ofzg", + "icon": "./icons/aliyun.webp" + }, + { + "id": "colocrossing", + "title": "colocrossing", + "subtitle": "美国 VPS 服务商", + "href": "https://cloud.colocrossing.com/aff.php?aff=1846", + "icon": "./icons/colocrossing.webp" + } + ] + }, + { + "id": "resi-ip", + "title": "静态住宅 IP", + "links": [ + { + "id": "iproyal", + "title": "iproyal", + "subtitle": "静态住宅 IP,出海必备", + "href": "https://iproyal.cn/?r=nomadro", + "icon": "./icons/iproyal.webp" + }, + { + "id": "proxy-cheap", + "title": "proxy-cheap", + "subtitle": "便宜的住宅 IP", + "href": "https://app.proxy-cheap.com/r/bmcMoY", + "icon": "./icons/proxy-cheap.webp" + }, + { + "id": "smartproxy", + "title": "smartproxy", + "subtitle": "静态住宅 IP 服务商", + "href": "https://www.smartproxy.org/register/?invitation_code=7RXPLM", + "icon": "./icons/smartproxy.webp" + } + ] + }, + { + "id": "resi-vps", + "title": "住宅 VPS 服务器", + "links": [ + { + "id": "lisa", + "title": "lisa", + "subtitle": "住宅 IP 服务器,TikTok 必备", + "href": "https://lisahost.com/aff.php?aff=7566", + "icon": "./icons/lisa.webp" + }, + { + "id": "voyracloud", + "title": "voyracloud", + "subtitle": "静态住宅 IP 服务器", + "href": "https://www.voyracloud.com/?ref_code=2LJPRTE5", + "icon": "./icons/voyracloud.webp" + } + ] + }, + { + "id": "hardware", + "title": "硬件", + "links": [ + { + "id": "rpi", + "title": "树莓派", + "subtitle": "无与伦比的 ARM 开发板", + "href": "https://uland.taobao.com/coupon/edetail?e=5hkWkiThOmClhHvvyUNXZfh8CuWt5YH5OVuOuRD5gLJMmdsrkidbOWBzzpT26idJPSbzO0ArF68ertbZl9wqRUbeJevxT02sHgNIxyFsmaMOsi2OxhyQb%2BhYUTTNN7GRRSHvQe2jOLZ9pbNCYX0I%2BPP%2BWUTgK%2F%2B0I%2BtaUgbudUxA%2B536asYsLWVfKa%2BhVnND4NyB1fwjO3bJLf667Yy8Q5jB6TX2HR3Qrq8LCw4Dujjqyaqc1rGLUwdQHHM8iY%2BUDMxLRSiIntldeBz90au3REAmOgZ0RtdnfeSBK3KXa0%2FA9snq37kKL6zL4bZ3EAS%2F2oYsLHTsIOxroXBFP6oz%2BA%3D%3D&traceId=213e032e17650447230113327e0c4f&union_lens=lensId%3APUB%401765044691%400b521400_0da4_19af4dca231_da77%400290EZjuZ2xbgOlH3NTYAyI%40eyJmbG9vcklkIjoyMDg3MTcxMDEwLCJzcG1CIjoiiX3BvcnRhbF92Ml90b29sX3NlbGVjdGlvbmxpc3RfZGV0YWlsX2luZGV4X2h0bSIsInNyY0Zsb29ySWQiiOiiI4MDY2NCJ9", + "icon": "./icons/rpi.webp" + }, + { + "id": "mini-pc", + "title": "迷你主机", + "subtitle": "具有性价比的 X86 小主机", + "href": "https://s.click.taobao.com/t?e=m%3D2%26s%3DCb1Quo%2F0Wb5w4vFB6t2Z2ueEDrYVVa64YUrQeSeIhnK53hKxp7mNFuhCZFSw9yq2woqx0EHqItb0JlhLk0Jl4eVWmNyHytKdTKJ80yOOA0Mtd0e5Ku7CIDk5ffgV5fRI1qrpxiwMoCNxc1AtbZGVS936JshS%2F%2FIdwgQgG5hIzyLNEPXytV9ALtCLThlbPuuZLb93Df8fOziGg7JVKZos%2BjLJJgRsfWlWtajntMnDLEHhuApO%2F8klJ62RdK%2B6%2FP7Z6k6s3rDgsC6jO9AJYjY8CXJ%2BwEVkOqHFPSJJXArlDuzso5nyPLY4NPiLqets6lIJ", + "icon": "./icons/mini-pc.webp" + }, + { + "id": "gpu", + "title": "GPU 显卡", + "subtitle": "AI 时代的算力基础设施", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATUJK1olXwEDUFZVCkkRC18IGloWWwALV19aAUonRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoUBWkBGFoSVAcdDRsBVXsQB29pbiZRKGMHKlclCEgVWghcGV91UQoyVW5dCUoWAmYPHl4RbTYCU24fZp-esLulkIK0yd6n5V1eOEonA2kPH1oQWQUFV19VCnsQA2Y4El0dWQ4LU1hBC00SCmcJK2slXjYFVFdJDjlWUXsOaWslXTYBZF5cCEoQAGoLH1kRQQYHV1lbAFcXBWgMGl4RXgUKUllYOEkWAmsBK2vL05J3IB9Uajl8UStqSz4dFQ9jitDJGTlnA2sPH0olImF5ECFaSU0VVAdNQDxuW09KFiw8SyljbW1jcih3JWNxNlwaS0hqWBhoX2sQbQEEVW4", + "icon": "./icons/gpu.webp" + } + ] + }, + { + "id": "software", + "title": "软件", + "links": [ + { + "id": "3x-ui", + "title": "3X-UI", + "subtitle": "科学上网仪表盘", + "href": "https://github.com/MHSanaei/3x-ui", + "icon": "./icons/3x-ui.webp" + }, + { + "id": "mihomo", + "title": "虚空终端 mihomo", + "subtitle": "代理软件以及配置 wiki", + "href": "https://wiki.metacubex.one/config/", + "icon": "./icons/mihomo.webp" + }, + { + "id": "frp", + "title": "frp", + "subtitle": "高性能内网穿透", + "href": "https://gofrp.org/zh-cn/docs/", + "icon": "./icons/frp.webp" + }, + { + "id": "wireguard", + "title": "wireguard", + "subtitle": "P2P 打洞组网", + "href": "https://github.com/WireGuard/wireguard-go", + "icon": "./icons/wireguard.webp" + }, + { + "id": "lineageos", + "title": "lineageos (pi5)", + "subtitle": "树莓派的安卓 LineageOS", + "href": "https://konstakang.com/devices/rpi5/", + "icon": "./icons/lineageos.webp" + } + ] + }, + { + "id": "gear", + "title": "数码装备", + "links": [ + { + "id": "ai-glasses", + "title": "AI 眼镜", + "subtitle": "第一视角拍摄", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATkJK1olXw8AXVxfCU8UA18IGloVWwcKXFlZDUgnRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoXBW4AE1wRWAUdDRsBVXtwXw1pZzNKAmZgFFwlYUh8UzVBZyZlUQoyVW5dCUoWAmYPHl4RbTYCU24fZpO9hbeBtYKjxt-jwInikZ-fjV8JK1sTWgIDUVtVAEkVA2w4HFscbQ8EXFpVAUwRH2wOHlIdXDYyZF1tD0seF2l6WgkBW3QyZF5tC3sXAm8AGFsVXAEBXEJdDUMVB24UG10SWQcHUVZbAE8fCl8KGloRVDYyitPtY0JrQAYIYj0dXGZKMVc5T5Was35jYlMVXQUTZAQOch9SZTMNbC9gIGdQJi5VCzkRSCpfWDUXVAJlDQoCbw1gcG9qRwYdHnYyUW5aDEgn", + "icon": "./icons/ai-glasses.webp" + }, + { + "id": "action-cam", + "title": "运动相机", + "subtitle": "骑行记录仪", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eCE0UB28ME1slGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWXQABUF5ZAEsIWipURmt3CFhXIQkpbChuRy8JGDpRJUJQDwkbBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVpaDEsUM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmsNHl8UXQQeVF1ZDE0UH28OHF8UWAIDUVddCkgnAW4JH1IlbdiMwDxcaAJfBTlNTRpCBHpHLhyDht8GcR8BHFIUTDZ6NSMpYRJweWtBfAUXC2JyXBkpfTNRWi1mGV4UAlgENFpeDA13cRRcTAJLbQMyU1peOA", + "icon": "./icons/action-cam.webp" + }, + { + "id": "drone", + "title": "无人机", + "subtitle": "天空视角", + "href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eAUIeB2YMH14lGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWVA8LUFdZDE4IWipURmtCHHVEABsFVChPYRJAbVhlGRhbFRgLBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVdbC04VM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmgLHlgRXgIeVFpfDU0UH28OHF8UWAIDUlteAUwnAW4JH1IlbdiMwCQvcipMBxJhfV9HGgF3FVaDht8GfxgBGVkRTDZUCjsVbzBTYRJbeRkWGG9WAx02D0JcUy5mGRNRFEJiUwgldStzfx1wTj1GbQMyU1peOA", + "icon": "./icons/drone.webp" + } + ] + } + ] + }, + { + "id": "overseas", + "title": "出海掘金", + "emoji": "◎", + "groups": [ + { + "id": "tiktok", + "title": "TikTok", + "links": [ + { + "id": "tiktok", + "title": "TikTok", + "subtitle": "全球直播平台", + "href": "https://www.tiktok.com/", + "icon": "./icons/tiktok.webp" + }, + { + "id": "cloud-phone", + "title": "云手机", + "subtitle": "用开源硬件打造云手机", + "href": "https://www.hackrobot.cn/", + "icon": "./icons/cloud-phone.webp" + }, + { + "id": "vcam", + "title": "vcam 虚拟摄像头", + "subtitle": "Xposed 模块,替换摄像头视频流", + "href": "https://pan.baidu.com/s/1NW4GMGA3cxfUQNWZPD9vOg?pwd=live", + "icon": "./icons/vcam.webp" + } + ] + }, + { + "id": "youtube", + "title": "YouTube", + "links": [ + { + "id": "youtube", + "title": "YouTube", + "subtitle": "全球最大的视频网站", + "href": "https://www.youtube.com/", + "icon": "./icons/youtube.webp" + }, + { + "id": "smmfollows", + "title": "smmfollows", + "subtitle": "刷粉刷量平台", + "href": "https://smmfollows.com/ref/dfugq", + "icon": "./icons/smmfollows.webp" + }, + { + "id": "yt-auto-dismiss", + "title": "YouTube Studio Auto Dismiss", + "subtitle": "油猴脚本,无人直播辅助", + "href": "https://greasyfork.org/zh-CN/scripts/557378-youtube-studio-auto-dismiss", + "icon": "./icons/yt-auto-dismiss.webp" + } + ] + }, + { + "id": "web", + "title": "Web 网站", + "links": [ + { + "id": "namesilo", + "title": "namesilo", + "subtitle": "注册域名", + "href": "https://www.namesilo.com/?rid=3bd4047mc", + "icon": "./icons/namesilo.webp" + }, + { + "id": "aapanel", + "title": "aapanel", + "subtitle": "宝塔面板", + "href": "https://www.aapanel.com?referral_code=wcn04vez", + "icon": "./icons/aapanel.webp" + }, + { + "id": "cloudflare", + "title": "Cloudflare", + "subtitle": "互联网基础节点", + "href": "https://www.cloudflare.com/", + "icon": "./icons/cloudflare.webp" + }, + { + "id": "ga", + "title": "Google Analytics", + "subtitle": "网站流量统计服务", + "href": "https://analytics.google.com/", + "icon": "./icons/ga.webp" + }, + { + "id": "adsense", + "title": "Google AdSense", + "subtitle": "Google 广告服务", + "href": "https://www.google.com/adsense/", + "icon": "./icons/adsense.webp" + } + ] + } + ] + }, + { + "id": "live", + "title": "无人直播", + "emoji": "▷", + "groups": [ + { + "id": "live-soft", + "title": "软件", + "links": [ + { + "id": "obs", + "title": "OBS", + "subtitle": "免费开源:录屏 / 推流直播", + "href": "https://pan.quark.cn/s/03e89f1bbc9b", + "icon": "./icons/obs.webp" + }, + { + "id": "ffmpeg", + "title": "ffmpeg", + "subtitle": "命令行视频处理 / 推流,Python 自动化", + "href": "https://www.ffmpeg.org/download.html", + "icon": "./icons/ffmpeg.webp" + }, + { + "id": "srs", + "title": "SRS", + "subtitle": "开源直播服务器,拉流推流中转", + "href": "https://ossrs.io/", + "icon": "./icons/srs.webp" + } + ] + } + ] + }, + { + "id": "pay", + "title": "支付网关", + "emoji": "◇", + "groups": [ + { + "id": "merchant", + "title": "小微商户", + "links": [ + { + "id": "nowpayments", + "title": "NOWPayments", + "subtitle": "加密货币收款", + "href": "https://account.nowpayments.io/create-account?link_id=3680795921", + "icon": "./icons/nowpayments.webp" + }, + { + "id": "creem", + "title": "Creem", + "subtitle": "软件出海收款,税务合规", + "href": "https://www.creem.io/", + "icon": "./icons/creem.webp" + }, + { + "id": "stripe", + "title": "Stripe", + "subtitle": "全球在线支付", + "href": "https://stripe.com/", + "icon": "./icons/stripe.webp" + }, + { + "id": "zpay", + "title": "z-pay", + "subtitle": "支持独角数卡 · 易支付", + "href": "https://z-pay.cn/?uid=22119", + "icon": "./icons/zpay.webp" + }, + { + "id": "xorpay", + "title": "xorpay", + "subtitle": "支持收银台支付", + "href": "https://xorpay.com?r=nomadro", + "icon": "./icons/xorpay.webp" + } + ] + } + ] + }, + { + "id": "books", + "title": "书籍", + "emoji": "◈", + "groups": [ + { + "id": "handbook", + "title": "手册", + "links": [ + { + "id": "nomad-book", + "title": "数字游民手册", + "subtitle": "复制链接后在小红书打开", + "href": "https://www.xiaohongshu.com/s/poster?bgimg=https%3A%2F%2Fsns-redskillhub-s1.xhscdn.com%2Fred_app_image%2F1040g4m8325ke1q36l0404a4druqrlvd5valvb80%3Fsign%3Dbc0c41c2e219f6ca904f52176bd17f54%26t%3D6c9a7a85&xhsshare=&appuid=5acc75ba4eacab15c751fda5&apptime=1790527237&deeplink=xhsdiscover%3A%2F%2FminiTool%2F6ab8984da1e3af0015584e92%3Fsource%3Dh5%26page_key%3D28%26xhsMpScreenMode%3Dfull%26share_id%3D5e90e5979b054c8baf8b3db4c8993d79%26share_channel%3Dcopy_link", + "icon": "./icons/nomad-book.webp", + "action": "copy" + } + ] + } + ] + }, + { + "id": "forum", + "title": "论坛社区", + "emoji": "◉", + "groups": [ + { + "id": "blog", + "title": "博客", + "links": [ + { + "id": "yidu", + "title": "异度世界", + "subtitle": "独立博客,Hugo 搭建", + "href": "https://blog.hackrobot.cn", + "icon": "./icons/yidu.webp" + }, + { + "id": "dsx", + "title": "大师兄", + "subtitle": "AI 实践", + "href": "https://dsx2016.com", + "icon": "./icons/dsx.webp" + } + ] + } + ] + } + ], + "brandIcon": "./icons/_brand.webp" +}; diff --git a/xhs/tool/styles.css b/xhs/tool/styles.css new file mode 100644 index 0000000..ced7909 --- /dev/null +++ b/xhs/tool/styles.css @@ -0,0 +1,944 @@ +:root, +[data-theme="light"] { + --bg: #e9eef4; + --bg-elevated: #f7fafc; + --ink: #15202b; + --ink-muted: #5b6b7c; + --ink-faint: #8a98a8; + --line: rgba(21, 32, 43, 0.08); + --surface: rgba(255, 255, 255, 0.88); + --surface-solid: #ffffff; + --accent: #0d8f7b; + --accent-deep: #0a6e5f; + --accent-soft: rgba(13, 143, 123, 0.12); + --glow-a: rgba(13, 143, 123, 0.2); + --glow-b: rgba(56, 120, 180, 0.14); + --shadow: 0 8px 24px rgba(21, 32, 43, 0.08); + --shadow-card: 0 1px 2px rgba(21, 32, 43, 0.04), 0 6px 16px rgba(21, 32, 43, 0.04); + --toast-bg: #15202b; + --toast-ink: #f4f7fa; + --chip: rgba(21, 32, 43, 0.045); + --chip-active: #15202b; + --chip-active-ink: #f7fafc; + --topbar-bg: rgba(233, 238, 244, 0.9); + --font: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", + "Microsoft YaHei", "Noto Sans SC", sans-serif; + --radius: 14px; + --app-pad-top: 80px; + --safe-bottom: env(safe-area-inset-bottom, 0px); +} + +[data-theme="dark"] { + --bg: #0a0f14; + --bg-elevated: #121820; + --ink: #e8eef4; + --ink-muted: #8b99a8; + --ink-faint: #5d6c7c; + --line: rgba(232, 238, 244, 0.1); + --surface: rgba(18, 24, 32, 0.92); + --surface-solid: #121820; + --accent: #3dceb8; + --accent-deep: #2aa892; + --accent-soft: rgba(61, 206, 184, 0.14); + --glow-a: rgba(61, 206, 184, 0.14); + --glow-b: rgba(70, 130, 200, 0.12); + --shadow: 0 10px 28px rgba(0, 0, 0, 0.35); + --shadow-card: 0 1px 0 rgba(255, 255, 255, 0.03), 0 8px 20px rgba(0, 0, 0, 0.28); + --toast-bg: #e8eef4; + --toast-ink: #0a0f14; + --chip: rgba(232, 238, 244, 0.06); + --chip-active: #e8eef4; + --chip-active-ink: #0a0f14; + --topbar-bg: rgba(10, 15, 20, 0.9); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + min-height: 100%; + background: var(--bg); + color: var(--ink); + font-family: var(--font); + -webkit-font-smoothing: antialiased; + -webkit-tap-highlight-color: transparent; +} + +button { + font: inherit; + color: inherit; + border: none; + background: none; + padding: 0; + cursor: pointer; +} + +img { + display: block; +} + +.app-shell { + position: relative; + min-height: 100vh; + isolation: isolate; +} + +.bg-glow { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + overflow: hidden; + background: + radial-gradient(680px 380px at 8% -12%, var(--glow-a), transparent 58%), + radial-gradient(560px 320px at 100% 4%, var(--glow-b), transparent 55%), + var(--bg); +} + +.orb { + position: absolute; + border-radius: 50%; + opacity: 0.5; +} + +.orb-a { + width: 200px; + height: 200px; + left: -60px; + top: 18%; + background: radial-gradient(circle, var(--glow-a), transparent 70%); +} + +.orb-b { + width: 240px; + height: 240px; + right: -80px; + top: 42%; + background: radial-gradient(circle, var(--glow-b), transparent 70%); +} + +.topbar { + position: sticky; + top: 0; + z-index: 30; + display: flex; + align-items: center; + gap: 10px; + padding: calc(var(--app-pad-top) + 8px) 12px 10px; + background: var(--topbar-bg); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + border-bottom: 1px solid var(--line); +} + +.icon-btn { + width: 36px; + height: 36px; + border-radius: 11px; + background: var(--surface); + border: 1px solid var(--line); + box-shadow: var(--shadow-card); + display: grid; + place-items: center; + flex-shrink: 0; + font-size: 1rem; +} + +.icon-btn:active { + transform: scale(0.94); +} + +.menu-bars { + display: flex; + flex-direction: column; + gap: 3.5px; + width: 14px; +} + +.menu-bars i { + display: block; + height: 1.5px; + border-radius: 1px; + background: var(--ink); +} + +.brand-mark { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1; +} + +.brand-logo { + width: 28px; + height: 28px; + border-radius: 8px; + object-fit: cover; + border: 1px solid var(--line); +} + +.brand-text { + min-width: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 2px; +} + +.brand-name { + margin: 0; + font-size: 1rem; + font-weight: 700; + letter-spacing: -0.02em; + line-height: 1.2; +} + +.brand-current { + margin: 0; + max-width: 100%; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: -0.01em; + line-height: 1.25; + color: var(--accent-deep); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +[data-theme="dark"] .brand-current { + color: var(--accent); +} + +.page { + position: relative; + z-index: 1; + padding: 12px 12px calc(72px + var(--safe-bottom)); +} + +.cat-panel + .cat-panel { + margin-top: 22px; +} + +.cat-panel { + scroll-margin-top: calc(var(--app-pad-top) + 52px); +} + +.cat-heading { + display: flex; + align-items: center; + gap: 8px; + margin: 0 0 10px; + padding-left: 10px; + position: relative; +} + +.cat-heading::before { + content: ""; + position: absolute; + left: 0; + top: 0.25em; + bottom: 0.25em; + width: 3px; + border-radius: 99px; + background: var(--accent); +} + +.cat-heading h2 { + margin: 0; + font-size: 1.05rem; + font-weight: 700; +} + +.cat-count { + font-size: 0.66rem; + font-weight: 700; + color: var(--accent-deep); + background: var(--accent-soft); + padding: 2px 7px; + border-radius: 999px; +} + +[data-theme="dark"] .cat-count { + color: var(--accent); +} + +.group-block + .group-block { + margin-top: 12px; +} + +.group-title { + margin: 0 0 7px; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ink-faint); +} + +.link-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 7px; +} + +.link-card { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-width: 0; + text-align: left; + padding: 9px; + border-radius: var(--radius); + background: var(--surface); + border: 1px solid var(--line); + box-shadow: var(--shadow-card); +} + +.link-card:active { + transform: scale(0.978); +} + +.link-icon-wrap { + width: 34px; + height: 34px; + border-radius: 10px; + overflow: hidden; + border: 1px solid var(--line); + background: var(--chip); + flex-shrink: 0; +} + +.link-icon { + width: 100%; + height: 100%; + object-fit: cover; +} + +.link-meta { + min-width: 0; + flex: 1; +} + +.link-title { + display: block; + font-size: 0.82rem; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.link-desc { + display: block; + margin-top: 2px; + font-size: 0.65rem; + color: var(--ink-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.footer { + margin-top: 28px; + padding-top: 14px; + border-top: 1px solid var(--line); + text-align: center; + font-size: 0.7rem; + color: var(--ink-faint); +} + +.drawer-root { + position: fixed; + inset: 0; + z-index: 60; + pointer-events: none; +} + +.drawer-root.open { + pointer-events: auto; +} + +.drawer-mask { + position: absolute; + inset: 0; + background: rgba(8, 12, 18, 0.46); + opacity: 0; + transition: opacity 0.22s ease; +} + +.drawer-root.open .drawer-mask { + opacity: 1; +} + +.drawer-panel { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: min(80vw, 300px); + background: var(--bg-elevated); + border-right: 1px solid var(--line); + box-shadow: var(--shadow); + transform: translateX(-104%); + transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1); + display: flex; + flex-direction: column; + padding: calc(var(--app-pad-top) + 4px) 0 calc(16px + var(--safe-bottom)); +} + +.drawer-root.open .drawer-panel { + transform: translateX(0); +} + +.drawer-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 4px 12px 12px; + border-bottom: 1px solid var(--line); +} + +.drawer-brand-wrap { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.drawer-brand-wrap img { + width: 28px; + height: 28px; + border-radius: 8px; + object-fit: cover; + border: 1px solid var(--line); +} + +.drawer-brand { + display: block; + font-weight: 700; + font-size: 1rem; +} + +.drawer-sub { + display: block; + margin-top: 2px; + font-size: 0.68rem; + color: var(--ink-muted); +} + +.drawer-nav { + padding: 12px 10px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +.drawer-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + text-align: left; + padding: 12px; + border-radius: 12px; + color: var(--ink-muted); + margin-bottom: 4px; +} + +.drawer-item.active { + background: var(--accent-soft); + color: var(--accent); +} + +.drawer-item-title { + flex: 1; + font-size: 0.92rem; + font-weight: 600; + color: inherit; +} + +.drawer-item.active .drawer-item-title { + color: var(--ink); +} + +.drawer-item-count { + font-size: 0.68rem; + font-weight: 650; + background: var(--chip); + color: var(--ink-faint); + padding: 2px 7px; + border-radius: 999px; +} + +.back-top { + position: fixed; + right: 14px; + bottom: calc(18px + var(--safe-bottom)); + width: 40px; + height: 40px; + border-radius: 14px; + background: var(--chip-active); + color: var(--chip-active-ink); + box-shadow: var(--shadow); + opacity: 0; + pointer-events: none; + transform: translateY(8px); + transition: opacity 0.2s ease, transform 0.2s ease; + z-index: 40; + font-size: 1rem; +} + +.back-top.show { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +/* Link detail sheet */ +.sheet-root { + position: fixed; + inset: 0; + z-index: 70; + pointer-events: none; +} + +.sheet-root.open { + pointer-events: auto; +} + +.sheet-mask { + position: absolute; + inset: 0; + background: rgba(8, 12, 18, 0.48); + opacity: 0; + transition: opacity 0.22s ease; +} + +.sheet-root.open .sheet-mask { + opacity: 1; +} + +.sheet-panel { + position: absolute; + left: 0; + right: 0; + bottom: 0; + max-height: min(86vh, 640px); + overflow-y: auto; + -webkit-overflow-scrolling: touch; + background: var(--bg-elevated); + border-radius: 20px 20px 0 0; + border: 1px solid var(--line); + border-bottom: none; + box-shadow: var(--shadow); + padding: 10px 16px calc(16px + var(--safe-bottom)); + transform: translateY(108%); + transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1); + display: flex; + flex-direction: column; + gap: 12px; +} + +.sheet-root.open .sheet-panel { + transform: translateY(0); +} + +.sheet-handle { + width: 36px; + height: 4px; + border-radius: 99px; + background: var(--chip); + margin: 0 auto 4px; +} + +.sheet-head { + display: flex; + align-items: center; + gap: 12px; +} + +.sheet-icon { + width: 44px; + height: 44px; + border-radius: 12px; + object-fit: cover; + border: 1px solid var(--line); + background: var(--chip); + flex-shrink: 0; +} + +.sheet-head-text { + min-width: 0; + flex: 1; +} + +.sheet-title { + margin: 0; + font-size: 1.05rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.sheet-sub { + margin: 4px 0 0; + font-size: 0.78rem; + color: var(--ink-muted); + line-height: 1.35; +} + +/* Visual guide steps */ +.guide-steps { + list-style: none; + margin: 0; + padding: 10px; + border-radius: 14px; + background: var(--chip); + border: 1px solid var(--line); + display: flex; + flex-direction: column; + gap: 8px; +} + +.guide-step { + display: flex; + align-items: flex-start; + gap: 10px; + opacity: 0.42; + transition: opacity 0.25s ease, transform 0.25s ease; + transform: translateX(0); +} + +.guide-step.is-active { + opacity: 1; + transform: translateX(2px); +} + +.guide-step.is-done { + opacity: 0.78; +} + +.guide-step.is-done .guide-num { + background: var(--accent); + color: #fff; +} + +.guide-num { + width: 24px; + height: 24px; + border-radius: 50%; + flex-shrink: 0; + display: grid; + place-items: center; + font-size: 0.75rem; + font-weight: 800; + background: var(--surface-solid); + border: 1px solid var(--line); + color: var(--ink-muted); +} + +.guide-step.is-active .guide-num { + background: var(--chip-active); + color: var(--chip-active-ink); + border-color: transparent; + box-shadow: 0 0 0 4px var(--accent-soft); + animation: num-pop 0.35s ease; +} + +.guide-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + padding-top: 1px; +} + +.guide-text strong { + font-size: 0.84rem; + font-weight: 750; + color: var(--ink); +} + +.guide-text em { + font-style: normal; + font-size: 0.72rem; + color: var(--ink-muted); +} + +.sheet-label { + margin: 0 0 8px; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ink-faint); +} + +.url-stage { + position: relative; +} + +.sheet-url-wrap { + position: relative; +} + +.sheet-url-box { + padding: 14px 12px; + border-radius: 14px; + background: color-mix(in srgb, var(--accent) 8%, var(--surface-solid)); + border: 2px solid color-mix(in srgb, var(--accent) 45%, var(--line)); + font-size: 0.86rem; + line-height: 1.5; + word-break: break-all; + user-select: text; + -webkit-user-select: text; + color: var(--ink); + transition: box-shadow 0.25s ease, border-color 0.25s ease, background 0.25s ease; + position: relative; + z-index: 1; +} + +.sheet-url-box.is-pulse { + animation: url-pulse 1.4s ease-in-out infinite; +} + +.sheet-url-box.is-selected { + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 16%, var(--surface-solid)); + box-shadow: 0 0 0 4px var(--accent-soft); + animation: none; +} + +.finger-ico { + font-size: 1.75rem; + line-height: 1; + filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.2)); +} + +.finger-btn.is-hidden, +.finger-url.is-hidden { + display: none !important; +} + +/* 第一步:仅小手,悬浮在绿色按钮上方,朝下指 */ +.sheet-actions-wrap { + position: relative; + padding-top: 36px; +} + +.finger-btn { + position: absolute; + left: 22%; + top: 0; + z-index: 3; + pointer-events: none; + display: flex; + align-items: center; + justify-content: center; + animation: finger-bounce-down 1.1s ease-in-out infinite; +} + +.finger-btn .finger-ico { + display: block; +} + +/* 第二步:小手紧贴网址区下沿朝上指,文案在右边 */ +.sheet-url-wrap.is-pointing { + padding-bottom: 34px; +} + +.finger-url { + position: absolute; + left: 6px; + top: auto; + bottom: 0; + z-index: 3; + pointer-events: none; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + max-width: calc(100% - 12px); + animation: finger-nudge-up 1.05s ease-in-out infinite; +} + +.finger-url .finger-ico { + flex-shrink: 0; +} + +.finger-url .finger-bubble { + max-width: 150px; + padding: 6px 9px; + border-radius: 10px; + background: var(--chip-active); + color: var(--chip-active-ink); + font-size: 0.68rem; + font-weight: 700; + line-height: 1.3; + text-align: left; + box-shadow: var(--shadow); + position: relative; +} + +.finger-url .finger-bubble::before { + content: ""; + position: absolute; + left: -4px; + top: 50%; + width: 8px; + height: 8px; + background: var(--chip-active); + transform: translateY(-50%) rotate(45deg); +} + +.sheet-status { + margin: 10px 0 0; + min-height: 1.2em; + font-size: 0.78rem; + font-weight: 650; + color: var(--accent-deep); + line-height: 1.4; +} + +[data-theme="dark"] .sheet-status { + color: var(--accent); +} + +.sheet-status.is-error { + color: #c44; +} + +.sheet-actions { + display: grid; + grid-template-columns: 1.7fr 0.9fr; + gap: 8px; +} + +.sheet-btn { + min-height: 48px; + border-radius: 14px; + border: 1px solid var(--line); + background: var(--surface); + font-size: 0.9rem; + font-weight: 650; + padding: 8px 10px; +} + +.sheet-btn.primary { + background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 88%, #fff), var(--accent-deep)); + color: #fff; + border-color: transparent; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; +} + +.sheet-btn.primary.pulse { + animation: cta-pulse 1.5s ease-in-out infinite; +} + +.sheet-btn.primary.is-done { + animation: none; + background: var(--chip); + color: var(--ink); + border: 1px solid var(--line); +} + +.sheet-btn-main { + font-size: 0.92rem; + font-weight: 800; +} + +.sheet-btn-sub { + font-size: 0.66rem; + font-weight: 600; + opacity: 0.88; +} + +.sheet-btn:active { + transform: scale(0.98); +} + +@keyframes url-pulse { + 0%, + 100% { + box-shadow: 0 0 0 0 var(--accent-soft); + } + 50% { + box-shadow: 0 0 0 6px var(--accent-soft); + } +} + +@keyframes cta-pulse { + 0%, + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(13, 143, 123, 0.35); + } + 50% { + transform: scale(1.015); + box-shadow: 0 0 0 8px rgba(13, 143, 123, 0); + } +} + +@keyframes finger-bounce-down { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(6px); + } +} + +@keyframes finger-nudge-up { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-6px); + } +} + +@keyframes num-pop { + from { + transform: scale(0.7); + } + to { + transform: scale(1); + } +} + +.toast { + position: fixed; + left: 50%; + bottom: calc(28px + var(--safe-bottom)); + transform: translateX(-50%) translateY(10px); + padding: 9px 16px; + border-radius: 999px; + background: var(--toast-bg); + color: var(--toast-ink); + font-size: 0.8rem; + font-weight: 600; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease; + z-index: 80; + box-shadow: var(--shadow); +} + +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +}